Skip to content

Commit 530f552

Browse files
committed
pal3: Refine shaders
1 parent 751c6d9 commit 530f552

4 files changed

Lines changed: 152 additions & 122 deletions

File tree

radiance/radiance/src/rendering/vulkan/shaders/openpal3/pal3_actor.frag

Lines changed: 12 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
#version 450
22
#extension GL_ARB_separate_shader_objects : enable
33

4-
// PAL3-specific actor (skin) shader — fragment stage. Faithfully reproduces
5-
// the original PAL3 `gfxscript/skin_lit2.cg` lighting model:
4+
// PAL3-specific actor (skin) shader — fragment stage. The lighting is computed
5+
// per-vertex in `pal3_actor.vert` (faithful to the original `vs_1_1`
6+
// skin_1L/skin_2L.gbf Gouraud model); this stage only performs the texture
7+
// combine the original effect script declares:
68
//
7-
// color = texture * ( SUM_2lights( atten * max(N·L, 0) * diffuse ) + ambient )
9+
// ColorOp[0] = Modulate; ColorArg1 = TEXTURE; ColorArg2 = CURRENT(=diffuse)
10+
// AlphaOp[0] = Modulate; AlphaArg1 = TEXTURE; AlphaArg2 = CURRENT
811
//
9-
// i.e. at most the two nearest omni point lights, a hard N·L clamp at 0 (no
10-
// fill/wrap floor), and ambient added flat. Point-light attenuation is the
11-
// Direct3D form 1/(a0 + a1·d + a2·d²); PAL3 ships FLT_MAX range so attenuation
12-
// is effectively 1 (the lights act near-directional). Diffuse is white via
13-
// the material tint. This is distinct from the shared `actor_lit` shader,
14-
// which sums all scene lights with a 0.2 wrap floor.
12+
// i.e. finalPixel = texture * interpolatedVertexColor. Computing the Lambert
13+
// term per-vertex (not per-pixel) is what gives the original its flat, evenly
14+
// lit characters.
1515

1616
layout(constant_id = 0) const bool ALPHA_TEST = true;
1717

@@ -39,7 +39,7 @@ layout(set = 3, binding = 0) uniform MaterialParams {
3939

4040
layout(location = 0) in vec2 fragTexCoord;
4141
layout(location = 1) in vec3 fragWorldPos;
42-
layout(location = 2) in vec3 fragNormal;
42+
layout(location = 2) in vec3 fragColor; // per-vertex Gouraud color (clamped)
4343

4444
layout(location = 0) out vec4 outColor;
4545

@@ -49,52 +49,8 @@ void main() {
4949
discard;
5050
}
5151

52-
vec3 N = normalize(fragNormal);
53-
int count = int(perFrameUbo.ambient.w);
54-
55-
// PAL3 actors carry a near-full material ambient (skin.gbf shows the
56-
// character fully visible on ambient alone); the scene's global ambient
57-
// floor is only ~0.1 and is meant for scenery. Lift it so roles match the
58-
// original's bright, evenly-lit look.
59-
vec3 ambient = max(perFrameUbo.ambient.rgb, vec3(0.55));
60-
61-
// Pick the two nearest enabled lights, matching the original engine's
62-
// 1-2 light skin shaders. The full table is at most 16 entries.
63-
int best0 = -1, best1 = -1;
64-
float d0 = 1e30, d1 = 1e30;
65-
for (int i = 0; i < count; i++) {
66-
float dist = distance(perFrameUbo.lightPos[i].xyz, fragWorldPos);
67-
if (dist < d0) {
68-
d1 = d0; best1 = best0;
69-
d0 = dist; best0 = i;
70-
} else if (dist < d1) {
71-
d1 = dist; best1 = i;
72-
}
73-
}
74-
75-
vec3 lit = ambient;
76-
int idx[2] = int[2](best0, best1);
77-
for (int k = 0; k < 2; k++) {
78-
int i = idx[k];
79-
if (i < 0) continue;
80-
vec3 d = perFrameUbo.lightPos[i].xyz - fragWorldPos;
81-
float dist = length(d);
82-
vec3 L = dist > 0.0 ? d / dist : vec3(0.0, 1.0, 0.0);
83-
84-
// PAL3 omni lights ship FLT_MAX range (no attenuation); treat any very
85-
// large outer radius as infinite, else use the D3D point-light cutoff.
86-
float outer = perFrameUbo.lightPos[i].w;
87-
float inner = perFrameUbo.lightColor[i].w;
88-
float atten = 1.0;
89-
if (outer < 1.0e18) {
90-
float edge0 = max(inner, outer * 0.85);
91-
atten = 1.0 - smoothstep(edge0, outer, dist);
92-
}
93-
94-
lit += perFrameUbo.lightColor[i].rgb * max(dot(N, L), 0.0) * atten;
95-
}
96-
97-
vec3 rgb = sampled.rgb * lit * mat.tint.rgb * mat.tint.a;
52+
// finalPixel = texture * interpolated vertex color (ColorOp = Modulate).
53+
vec3 rgb = sampled.rgb * fragColor * mat.tint.rgb * mat.tint.a;
9854
outColor = vec4(rgb, sampled.a * mat.tint.a);
9955

10056
// Linear distance fog, matching the shared shaders' eye-space fog.

radiance/radiance/src/rendering/vulkan/shaders/openpal3/pal3_actor.vert

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
11
#version 450
22
#extension GL_ARB_separate_shader_objects : enable
33

4-
// PAL3-specific actor (skin) shader — vertex stage. Mirrors the original
5-
// PAL3 `gfxscript/skin_lit*.cg` / `skin_*.gbf` model: lighting is computed
6-
// per-vertex and modulated by the texture in the fragment stage. World/normal
7-
// transform follows radiance's row-vector convention (`v * M`).
4+
// PAL3-specific actor (skin) shader — vertex stage. Faithfully reproduces the
5+
// original PAL3 `gfxscript/skin_1L.gbf` / `skin_2L.gbf` (`vs_1_1`) lighting
6+
// model, which is computed **per vertex** (Gouraud) and modulated by the
7+
// texture in the fragment stage:
8+
//
9+
// perLight_i = atten_i * max(N·L_i, 0) * diffuse_i.rgb + ambient_i.rgb
10+
// color.rgb = saturate( SUM_{i in 1..2} perLight_i ) // vs_1_1 clamps COLOR
11+
// color.a = diffuse[0].w
12+
// finalPixel = texture * color // ColorOp = Modulate
13+
//
14+
// The original evaluates at most the two nearest omni point lights with the
15+
// Direct3D attenuation `atten = 1/(a0 + a1·d + a2·d²)`. PAL3 ships un-attenuated
16+
// omni lights (FLT_MAX range ⇒ `a1=a2=0` ⇒ atten≈1). Doing this per-vertex —
17+
// rather than per-pixel — is what gives the original its characteristically
18+
// flat, evenly-lit characters (a per-pixel N·L looks "too realistic").
19+
//
20+
// The `ambient` term stands in for the character's material ambient
21+
// (`MtlAmbient`), which is high for PAL3 roles (skin is fully readable on
22+
// ambient alone); it is distinct from the dim ~0.10 scene ambient used for
23+
// scenery, hence the 0.55 floor. World/normal transform follows radiance's
24+
// row-vector convention (`v * M`).
825
layout(set = 0, binding = 0) uniform PerFrameUbo {
926
mat4 view;
1027
mat4 proj;
@@ -29,7 +46,7 @@ layout(location = 2) in vec2 inTexCoord;
2946

3047
layout(location = 0) out vec2 fragTexCoord;
3148
layout(location = 1) out vec3 fragWorldPos;
32-
layout(location = 2) out vec3 fragNormal;
49+
layout(location = 2) out vec3 fragColor;
3350

3451
mat4 clip = mat4(vec4(1.0, 0.0, 0.0, 0.0),
3552
vec4(0.0, -1.0, 0.0, 0.0),
@@ -40,7 +57,56 @@ void main() {
4057
vec4 world = vec4(position, 1.0) * perInstanceUbo.model;
4158
gl_Position = world * perFrameUbo.view * perFrameUbo.proj * clip;
4259

43-
fragWorldPos = world.xyz;
44-
fragNormal = normalize(normal * mat3(perInstanceUbo.model));
60+
vec3 worldPos = world.xyz;
61+
vec3 N = normalize(normal * mat3(perInstanceUbo.model));
62+
63+
// Character material ambient: high (skin reads fully on ambient), and
64+
// separate from the dim scene ambient used for scenery.
65+
vec3 ambient = max(perFrameUbo.ambient.rgb, vec3(0.55));
66+
67+
// Pick the two nearest enabled lights, matching the original's 1-2 light
68+
// skin shaders. The full table is at most 16 entries.
69+
int count = int(perFrameUbo.ambient.w);
70+
int best0 = -1, best1 = -1;
71+
float d0 = 1e30, d1 = 1e30;
72+
for (int i = 0; i < count; i++) {
73+
float dist = distance(perFrameUbo.lightPos[i].xyz, worldPos);
74+
if (dist < d0) {
75+
d1 = d0; best1 = best0;
76+
d0 = dist; best0 = i;
77+
} else if (dist < d1) {
78+
d1 = dist; best1 = i;
79+
}
80+
}
81+
82+
// Per-vertex Gouraud accumulation, exactly as skin_1L/skin_2L.gbf.
83+
vec3 lit = ambient;
84+
int idx[2] = int[2](best0, best1);
85+
for (int k = 0; k < 2; k++) {
86+
int i = idx[k];
87+
if (i < 0) continue;
88+
vec3 d = perFrameUbo.lightPos[i].xyz - worldPos;
89+
float dist = length(d);
90+
vec3 L = dist > 0.0 ? d / dist : vec3(0.0, 1.0, 0.0);
91+
92+
// PAL3 omni lights ship FLT_MAX range (no attenuation); treat any very
93+
// large outer radius as infinite, else use the D3D point-light cutoff.
94+
float outer = perFrameUbo.lightPos[i].w;
95+
float inner = perFrameUbo.lightColor[i].w;
96+
float atten = 1.0;
97+
if (outer < 1.0e18) {
98+
float edge0 = max(inner, outer * 0.85);
99+
atten = 1.0 - smoothstep(edge0, outer, dist);
100+
}
101+
102+
lit += perFrameUbo.lightColor[i].rgb * max(dot(N, L), 0.0) * atten;
103+
}
104+
105+
// vs_1_1 clamps the interpolated COLOR output to [0,1] before it reaches the
106+
// texture stage; replicate that here so bright keys don't blow out the
107+
// texture modulation.
108+
fragColor = clamp(lit, 0.0, 1.0);
109+
110+
fragWorldPos = worldPos;
45111
fragTexCoord = inTexCoord * mat.uv_xform.xy + mat.uv_xform.zw;
46112
}

radiance/radiance/src/rendering/vulkan/shaders/openpal3/pal3_geom.frag

Lines changed: 9 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
#version 450
22
#extension GL_ARB_separate_shader_objects : enable
33

4-
// PAL3 static geometry shader (POL/CVD) — fragment stage. Reproduces the
5-
// original PAL3 `gfxscript/geom_1L.cg` / `geom_2L` lighting model:
6-
//
7-
// color = texture * ( SUM_2lights( atten * max(N·L, 0) * diffuse ) + ambient )
8-
//
9-
// i.e. at most the two nearest omni point lights, a hard N·L clamp at 0 (no
10-
// fill/wrap floor), and ambient added flat. Point-light attenuation is the
11-
// Direct3D form 1/(a0 + a1·d + a2·d²); PAL3 ships FLT_MAX range so attenuation
12-
// is effectively 1 (the lights act near-directional). Diffuse is white via
13-
// the material tint. This is distinct from the shared `actor_lit` shader,
14-
// which sums all scene lights with a 0.2 wrap floor.
4+
// PAL3 static geometry shader (POL/CVD) — fragment stage. Lighting is computed
5+
// per-vertex in `pal3_geom.vert` (faithful to the original `vs_1_1`
6+
// geom_1L/geom_2L.gbf Gouraud model); this stage only performs the texture
7+
// combine the effect script declares: finalPixel = texture * vertexColor
8+
// (ColorOp[0] = Modulate). Per-vertex (not per-pixel) shading is what keeps the
9+
// original's flat look.
1510

1611
layout(constant_id = 0) const bool ALPHA_TEST = true;
1712

@@ -39,7 +34,7 @@ layout(set = 3, binding = 0) uniform MaterialParams {
3934

4035
layout(location = 0) in vec2 fragTexCoord;
4136
layout(location = 1) in vec3 fragWorldPos;
42-
layout(location = 2) in vec3 fragNormal;
37+
layout(location = 2) in vec3 fragColor; // per-vertex Gouraud color (clamped)
4338

4439
layout(location = 0) out vec4 outColor;
4540

@@ -49,46 +44,8 @@ void main() {
4944
discard;
5045
}
5146

52-
vec3 N = normalize(fragNormal);
53-
int count = int(perFrameUbo.ambient.w);
54-
55-
// Pick the two nearest enabled lights, matching the original engine's
56-
// 1-2 light skin shaders. The full table is at most 16 entries.
57-
int best0 = -1, best1 = -1;
58-
float d0 = 1e30, d1 = 1e30;
59-
for (int i = 0; i < count; i++) {
60-
float dist = distance(perFrameUbo.lightPos[i].xyz, fragWorldPos);
61-
if (dist < d0) {
62-
d1 = d0; best1 = best0;
63-
d0 = dist; best0 = i;
64-
} else if (dist < d1) {
65-
d1 = dist; best1 = i;
66-
}
67-
}
68-
69-
vec3 lit = perFrameUbo.ambient.rgb;
70-
int idx[2] = int[2](best0, best1);
71-
for (int k = 0; k < 2; k++) {
72-
int i = idx[k];
73-
if (i < 0) continue;
74-
vec3 d = perFrameUbo.lightPos[i].xyz - fragWorldPos;
75-
float dist = length(d);
76-
vec3 L = dist > 0.0 ? d / dist : vec3(0.0, 1.0, 0.0);
77-
78-
// PAL3 omni lights ship FLT_MAX range (no attenuation); treat any very
79-
// large outer radius as infinite, else use the D3D point-light cutoff.
80-
float outer = perFrameUbo.lightPos[i].w;
81-
float inner = perFrameUbo.lightColor[i].w;
82-
float atten = 1.0;
83-
if (outer < 1.0e18) {
84-
float edge0 = max(inner, outer * 0.85);
85-
atten = 1.0 - smoothstep(edge0, outer, dist);
86-
}
87-
88-
lit += perFrameUbo.lightColor[i].rgb * max(dot(N, L), 0.0) * atten;
89-
}
90-
91-
vec3 rgb = sampled.rgb * lit * mat.tint.rgb * mat.tint.a;
47+
// finalPixel = texture * interpolated vertex color (ColorOp = Modulate).
48+
vec3 rgb = sampled.rgb * fragColor * mat.tint.rgb * mat.tint.a;
9249
outColor = vec4(rgb, sampled.a * mat.tint.a);
9350

9451
// Linear distance fog, matching the shared shaders' eye-space fog.

radiance/radiance/src/rendering/vulkan/shaders/openpal3/pal3_geom.vert

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
#version 450
22
#extension GL_ARB_separate_shader_objects : enable
33

4-
// PAL3 static geometry shader (POL/CVD) — vertex stage. Mirrors the original
5-
// PAL3 `gfxscript/skin_lit*.cg` / `skin_*.gbf` model: lighting is computed
6-
// per-vertex and modulated by the texture in the fragment stage. World/normal
7-
// transform follows radiance's row-vector convention (`v * M`).
4+
// PAL3 static geometry shader (POL/CVD) — vertex stage. Faithfully reproduces
5+
// the original PAL3 `gfxscript/geom_1L.gbf` / `geom_2L.gbf` (`vs_1_1`) model:
6+
// lighting is computed **per vertex** (Gouraud) and modulated by the texture in
7+
// the fragment stage, exactly like the skin shaders but without bone skinning:
8+
//
9+
// perLight_i = atten_i * max(N·L_i, 0) * diffuse_i.rgb + ambient.rgb
10+
// color.rgb = saturate( SUM_{i in 1..2} perLight_i ) // vs_1_1 clamps COLOR
11+
// finalPixel = texture * color // ColorOp = Modulate
12+
//
13+
// World/normal transform follows radiance's row-vector convention (`v * M`).
14+
// Lightmapped POL surfaces use `LightMapMaterialDef` instead of this path.
815
layout(set = 0, binding = 0) uniform PerFrameUbo {
916
mat4 view;
1017
mat4 proj;
@@ -29,7 +36,7 @@ layout(location = 2) in vec2 inTexCoord;
2936

3037
layout(location = 0) out vec2 fragTexCoord;
3138
layout(location = 1) out vec3 fragWorldPos;
32-
layout(location = 2) out vec3 fragNormal;
39+
layout(location = 2) out vec3 fragColor;
3340

3441
mat4 clip = mat4(vec4(1.0, 0.0, 0.0, 0.0),
3542
vec4(0.0, -1.0, 0.0, 0.0),
@@ -40,7 +47,51 @@ void main() {
4047
vec4 world = vec4(position, 1.0) * perInstanceUbo.model;
4148
gl_Position = world * perFrameUbo.view * perFrameUbo.proj * clip;
4249

43-
fragWorldPos = world.xyz;
44-
fragNormal = normalize(normal * mat3(perInstanceUbo.model));
50+
vec3 worldPos = world.xyz;
51+
vec3 N = normalize(normal * mat3(perInstanceUbo.model));
52+
53+
// Scenery uses the dim scene ambient directly (unlike actors, which carry a
54+
// high material ambient).
55+
vec3 ambient = perFrameUbo.ambient.rgb;
56+
57+
// Pick the two nearest enabled lights (geom_1L/geom_2L).
58+
int count = int(perFrameUbo.ambient.w);
59+
int best0 = -1, best1 = -1;
60+
float d0 = 1e30, d1 = 1e30;
61+
for (int i = 0; i < count; i++) {
62+
float dist = distance(perFrameUbo.lightPos[i].xyz, worldPos);
63+
if (dist < d0) {
64+
d1 = d0; best1 = best0;
65+
d0 = dist; best0 = i;
66+
} else if (dist < d1) {
67+
d1 = dist; best1 = i;
68+
}
69+
}
70+
71+
// Per-vertex Gouraud accumulation, exactly as geom_1L/geom_2L.gbf.
72+
vec3 lit = ambient;
73+
int idx[2] = int[2](best0, best1);
74+
for (int k = 0; k < 2; k++) {
75+
int i = idx[k];
76+
if (i < 0) continue;
77+
vec3 d = perFrameUbo.lightPos[i].xyz - worldPos;
78+
float dist = length(d);
79+
vec3 L = dist > 0.0 ? d / dist : vec3(0.0, 1.0, 0.0);
80+
81+
float outer = perFrameUbo.lightPos[i].w;
82+
float inner = perFrameUbo.lightColor[i].w;
83+
float atten = 1.0;
84+
if (outer < 1.0e18) {
85+
float edge0 = max(inner, outer * 0.85);
86+
atten = 1.0 - smoothstep(edge0, outer, dist);
87+
}
88+
89+
lit += perFrameUbo.lightColor[i].rgb * max(dot(N, L), 0.0) * atten;
90+
}
91+
92+
// vs_1_1 clamps the interpolated COLOR output to [0,1].
93+
fragColor = clamp(lit, 0.0, 1.0);
94+
95+
fragWorldPos = worldPos;
4596
fragTexCoord = inTexCoord * mat.uv_xform.xy + mat.uv_xform.zw;
4697
}

0 commit comments

Comments
 (0)