Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Replacement shaders

This page is for modders who want to change how the game looks by writing their own shading programs. You don’t need it to play.

You can put your own shader files in the game folder, and the patch uses them in place of the programs it generates. You don’t need to rebuild anything or use any tools. Each file is plain text in ARB fragment program assembly, the same language the patch’s own programs use.

Where replacement files go

Put replacement files in override/shaders/ in the game folder. If the folder doesn’t exist, create it.

The folder is inside override so that a mod can install replacement shaders with TSLPatcher, like any other override file. The files are in a subfolder because they aren’t game resources. The game never reads them: it searches override for the file types it knows, it doesn’t search subfolders, and .arb isn’t one of its file types. Only the patch reads them.

How to name a replacement file

Each file replaces one program and is named after it:

  • World shaders, which draw surfaces, take a fog variant: <name>_<fog>.arb, where <fog> is off, linear, exp, or exp2.
  • Effect shaders, which draw screen effects, are named <name>.arb. Fog never reaches screen effects, so they have no fog variants.
  • brightness.arb applies the brightness slider to the finished frame. It reads the frame on texture unit 0 and the slider’s curve on unit 1, with one texel for each byte value.
  • downsample.arb shrinks the images for the glow and soft shadows. It reads the larger image on texture unit 0, and the values that Downsample::locals describes from program.local[0] to program.local[3].

The patch ignores capitalisation in file names, so a file named on Linux loads on Windows, and the reverse.

The program names are:

flat                 diffuseBlend          lightmap          envMap2D
envMapCube           envThreeTex2D         envThreeTexCube   envMapBlend2D
envMapBlendCube      normalTransform       bumpShinyA        bumpShinyB
lightmapBumpDiffuse  lightmapBumpSpecular1

overbrighten         forceDistortion       filmNoise         accumulationBlur
saturation           perPixelFlare         decal             decal4
softShadow

brightness           downsample

These are the names the log uses. To find the program a surface uses, set log = debug. The log then records each world program and fog variant the first time the patch binds it, and each effect program the first time the game uses it. For example, a line that contains probe mode=1A shader=lightmap fog=Linear means that the file to write is lightmap_linear.arb, and a line that contains probe effect=filmNoise means filmNoise.arb.

What a replacement file contains

Each file is a complete program, from !!ARBfp1.0 to END. The patch adds nothing to it. In particular, each world shader variant applies fog itself, which is why there are four variants. If you replace a fogged variant, your program must apply the fog, or that surface stops fogging.

Three world shaders are an exception: diffuseBlend, envMapBlend2D, and envMapBlendCube already fade towards the fog colour in their own code, as the game’s programs do. All four variants of each are the same program, without the fog step in the following example. The game turns the fixed function fog off while it draws these three, so only the _off variant of each ever runs. If you replace one of those three, write <name>_off.arb and leave fog alone, or the surface fades to the fog colour twice.

This is the generated lightmap_linear.arb for the Radeon look. The last five instructions apply the fog:

!!ARBfp1.0
PARAM fogParam = state.fog.params;
PARAM fogColour = state.fog.color;
TEMP fogCoord, fogFactor;
TEMP oCol;
TEMP base, lmap, sum;
TEX base, fragment.texcoord[0], texture[0], 2D;
TEX lmap, fragment.texcoord[1], texture[1], 2D;
ADD sum, fragment.color.primary, lmap;
MUL oCol.rgb, base, sum;
MUL oCol.a, fragment.color.primary, base;
ABS fogCoord.x, fragment.fogcoord.x;
SUB fogFactor.x, fogParam.z, fogCoord.x;
MUL_SAT fogFactor.x, fogFactor.x, fogParam.w;
LRP result.color.rgb, fogFactor.x, oCol, fogColour;
MOV result.color.a, oCol;
END

In the GeForce look, the default, ADD is ADD_SAT. That is the only difference between the two looks in this program.

The text of every world and effect program for the Radeon look is in the repository, in crates/arbfp-shaders/tests/golden/. A replacement is easiest to start from the program it replaces.

Programs that differ between the two looks

Nine programs differ between the two looks:

  • lightmap, envThreeTex2D, envThreeTexCube, envMap2D, envMapCube, envMapBlend2D, and envMapBlendCube limit a step that can pass full brightness. This is the brightness difference that Choose between the two looks describes.
  • bumpShinyA and bumpShinyB limit two dot products to zero or above, as the register combiners do, before the highlight is squared. Without that limit, a negative value becomes positive when squared, and lights the side of a surface that faces away from the light. The Hutts in the Taris cantinas show this difference, most on the parts that face away from the light. The GeForce versions also drop the highlight where the half vector points into the surface.

Every other program is the same in both looks. This includes lightmapBumpDiffuse and lightmapBumpSpecular1: the game has no ATI version of those modes, so both looks use the translation of the register combiner setup.

A replacement file replaces its program in both looks, because file names don’t include the look. To keep one look’s behaviour for one of the nine programs, write the replacement from that look’s version.

What your program receives

A replacement changes only the program text. The patch binds the same textures and sets the same constants as for its own program.

Your program receives two kinds of input:

  • Textures, on numbered texture units. Read them with TEX dst, fragment.texcoord[n], texture[n], 2D, or CUBE or 1D for those targets.
  • Values the game computes for each draw, as program.env[n]. Your program can read these but not write them.

World programs:

ProgramTexture unitsprogram.env[n]
flatNoneNone
diffuseBlend0 diffuse[1] colour to blend towards
lightmap0 diffuse, 1 lightmapNone
envMap2D0 diffuse, 1 environment[0].a output alpha
envMapCube0 diffuse, 1 environment (cube)[0].a output alpha
envMapBlend2D0 diffuse, 1 environment[0].a output alpha, [1] blend colour
envMapBlendCube0 diffuse, 1 environment (cube)[0].a output alpha, [1] blend colour
envThreeTex2D0 diffuse, 1 lightmap, 2 environment[0].a output alpha
envThreeTexCube0 diffuse, 1 lightmap, 2 environment (cube)[0].a output alpha
normalTransform0 normal map, 1 to 3 basis (cube), 4 refinement (cube), 5 environment (cube)[0].a output alpha
bumpShinyA0 normal map, 1 to 2 basis (cube), 3 modulating[0], [1] light colours
bumpShinyB0 modulating, 1 normal map, 2 to 3 basis (cube)[0], [1] light colours
lightmapBumpDiffuse0 normal map, 1 light vector (cube), 2 lightmap, 3 diffuse[1] light colour
lightmapBumpSpecular10 normal map, 1 half vector (cube), 2 lightmap, 3 diffuse[0] light colour

Effect programs, each of which shades a rectangle that covers the screen:

ProgramTexture unitsprogram.env[n]Output alpha
overbrighten0 frame, 1 to 3 tone curve tables (1D)[0] to [2] colour matrix rowsPrimary colour alpha
perPixelFlare0 frame, 1 to 3 tone curve tables (1D)[0] to [2] colour matrix rows1
forceDistortion0 normal map, 2 frameNoneNormal map alpha
filmNoise0 frame, 1 noiseNone1
accumulationBlur0 frame, 1 accumulation buffer[0], [1] weights[0].a
saturation0 frame[0] to [2] luminance rows, [3] to [5] weights for each channelFrame alpha
decal0 frameNone1
decal40 to 3, four samples of the frame[0] to [3] sample weightsPrimary colour alpha
softShadow0 to 3, four samples of the frame[0] to [3] sample weightsThe weighted sum’s alpha

Inputs to check

  • Name the texture target that is bound. Each TEX instruction names the target it samples, and the wrong target isn’t a compile error. The tone curve tables on units 1 to 3 of overbrighten and perPixelFlare are 1D, not 2D. If you name 2D, the program samples an incomplete target, and several drivers return white. The same applies to every CUBE in the preceding tables, which is why the environment mapped programs come in 2D and Cube pairs. The brightness curve on unit 1 of brightness is 1D as well.
  • In the reflective programs, the alpha of the diffuse texture is the reflectivity mask. An alpha of 1 keeps the lit texel, and 0 shows the reflection.
  • forceDistortion reads the frame on unit 2, not unit 0. Unit 0 is the normal map, and fragment.texcoord[1] and fragment.texcoord[2] are the basis vectors it takes dot products with.
  • Write the alpha the original writes. The game blends many draws by the alpha a program writes, so a replacement that writes a different alpha changes how its surface or effect blends, and can stop a surface fading. The effect table gives the alpha each effect writes, and the world table marks the programs that take theirs from program.env[0].a. For the rest, read the alpha from the patch’s own version of the program.
  • The interpolated colours are inputs too. fragment.color.primary is the lit vertex colour, and fragment.color.secondary.r is the amount the blend modes fade by. In the two lightmapped bump programs, the secondary colour is the light’s attenuation.

Check that your replacement loaded

When the patch loads a replacement, it writes a line like this to the log:

[    0.412] [INFO] program override envMapCube_linear.arb

If a file’s name doesn’t match any program, the patch writes a warning instead. A misnamed replacement looks the same as no replacement in the game, so this warning is how you find one:

[    0.412] [WARN] program override envMapCube.arb matches no program and was ignored

If the driver refuses to compile a replacement, the log reports the position where compiling stopped and the driver’s message. The patch doesn’t fall back to its own program. What happens instead depends on the program:

  • A refused world program leaves the surfaces that use it drawn without shading.
  • A refused effect program leaves that effect without a program.
  • A refused brightness program leaves the frame without the brightness pass.
  • A refused downsample program leaves the glow and soft shadows to the game’s own shrink.

Known limitations

  • overbrighten is never used. The game uses it only when a flag at 0x007A68BC is set, and nothing in the game sets that flag. A replacement for overbrighten has no effect.
  • bumpShinyB draws only normal mapped surfaces, whose bump map is an isbumpmap 2 texture at 32 bits per texel, on meshes without tangents. The game reaches it through Material::HasNormalMap, which every build of the game compiles to return false. The patch answers that function, so the program runs for such a mesh. No mesh in the shipped game has a normal map, so a replacement for bumpShinyB shows only on mod content.
  • lightmapBumpDiffuse and lightmapBumpSpecular1 draw only lightmapped bump mapped surfaces. In the shipped game, only a test model that no level loads has one, so a replacement for either shows only on mod content.
  • Only the off and linear fog variants have been seen in play. The patch still builds and tests the exp and exp2 variants.

Programs from other games

These programs aren’t the game’s own shaders. They’re translations of the game’s ATI_fragment_shader constructions and register combiner setups, so they expect this game’s texture units and constants, as the preceding tables list them. A program written for another game, including KOTOR 2, needs remapping to those inputs before it renders correctly here.