Lighting: Take the specular viewer direction from the view matrix

The viewer is at infinity along view space +z, so in world space, where
lighting happens, it's the view matrix's third column rather than
(0,0,1): turning the camera moves the highlights (gpu/lighting/specular).
The shaders read it from u_view.

In a Need for Speed Carbon frame replayed on a PSP, this and the pow
bring the error of the cars from MSE 957 to 120 (Vulkan).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-09-30 15:47:16 -06:00
1 parent 48f3d5c907
commit 6f6d3e2a81
6 files changed
+22 -11

No files matched your search

+2 -7
View File
@@ -41,7 +41,7 @@ Lighter::Lighter(int vertType) {
materialSpecular.GetFromRGB(gstate.materialspecular);
materialSpecular.a = 1.0f;
specCoef_ = PSPSpecularCoef(getFloat24(gstate.materialspecularcoef));
// viewer_ = Vec3f(-gstate.viewMatrix[9], -gstate.viewMatrix[10], -gstate.viewMatrix[11]);
viewDir_ = PSPViewDirection(gstate.viewMatrix);
bool hasColor = (vertType & GE_VTYPE_COL_MASK) != 0;
materialUpdate_ = hasColor ? (gstate.materialupdate & 7) : 0;
@@ -148,13 +148,8 @@ void Lighter::Light(float colorOut0[4], float colorOut1[4], const float colorIn[
Color4 lightDiff(lcolor[1][l], 0.0f);
Color4 diff = (lightDiff * *diffuse) * dot;
// Real PSP specular
static const Vec3f toViewer(0, 0, 1);
// Better specular
// Vec3f toViewer = (viewer - pos).NormalizedOr001(cpu_info.bSSE4_1);
if (doSpecular && facingLight) {
Vec3f halfVec = (toLight + toViewer).NormalizedOr001(cpu_info.bSSE4_1);
Vec3f halfVec = (toLight + viewDir_).NormalizedOr001(cpu_info.bSSE4_1);
dot = Dot(halfVec, norm);
if (dot > 0.0f) {
+6 -1
View File
@@ -85,6 +85,11 @@ inline float PSPSpecularCoef(float e) {
return e;
}
// The viewer is at infinity along view space +z, so in world space it's the view matrix's third column.
inline Vec3f PSPViewDirection(const float viewMatrix[12]) {
return Vec3f(viewMatrix[2], viewMatrix[5], viewMatrix[8]).NormalizedOr001(false);
}
// Convenient way to do precomputation to save the parts of the lighting calculation
// that's common between the many vertices of a draw call.
class Lighter {
@@ -106,7 +111,7 @@ private:
Color4 materialDiffuse;
Color4 materialSpecular;
float specCoef_;
// Vec3f viewer_;
Vec3f viewDir_;
bool doShadeMapping_;
int materialUpdate_;
+10 -2
View File
@@ -500,6 +500,14 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag
} else {
WRITE(p, " mediump vec3 worldnormal = normalizeOr001(mul(vec4(0.0, 0.0, %s1.0, 0.0), u_world).xyz);\n", flipNormal ? "-" : "");
}
if (enableLighting) {
// The viewer is at infinity along view space +z: in world space, the view matrix's third column.
if (compat.shaderLanguage == HLSL_D3D11) {
WRITE(p, " mediump vec3 viewDir = normalizeOr001(vec3(u_view[0].z, u_view[1].z, u_view[2].z));\n");
} else {
WRITE(p, " mediump vec3 viewDir = normalizeOr001(u_view[2].xyz);\n");
}
}
WRITE(p, " vec4 viewPos = vec4(mul(vec4(worldpos, 1.0), u_view).xyz, 1.0);\n");
if (useSimpleStereo) {
@@ -668,7 +676,7 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag
p.F(" diffuse = (u_lightdiffuse%s * diffuseColor) * max(ldot, 0.0);\n", iStr);
p.C(" if (comp == 0x1u && ldot >= 0.0) {\n"); // do specular. note - must allow for the >= case, since the u_matspecular.a <= 0.0 case relies on it.
p.C(" if (u_matspecular.a > 0.0) {\n");
p.C(" vec3 halfVec = toLight + vec3(0.0, 0.0, 1.0);\n");
p.C(" vec3 halfVec = toLight + viewDir;\n");
p.C(" float halfInvLen = inversesqrt(dot(halfVec, halfVec));\n");
p.C(" ldot = pspPow(dot(halfVec, worldnormal) * halfInvLen, u_matspecular.a);\n");
p.C(" } else {\n");
@@ -748,7 +756,7 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag
if (doSpecular) {
p.C(" if (ldot >= 0.0) {\n");
p.C(" if (u_matspecular.a > 0.0) {\n");
p.C(" vec3 halfVec = toLight + vec3(0.0, 0.0, 1.0);\n");
p.C(" vec3 halfVec = toLight + viewDir;\n");
p.C(" float halfInvLen = inversesqrt(dot(halfVec, halfVec));\n");
p.C(" ldot = pspPow(dot(halfVec, worldnormal) * halfInvLen, u_matspecular.a);\n");
p.C(" } else {\n");
+1 -1
View File
@@ -369,7 +369,7 @@ static void ProcessSIMD(VertexData &vertex, const WorldCoords &worldpos, const W
}
if (lstate.specular && diffuse_factor >= 0.0f) {
Vec3<float> H = L + Vec3<float>(0.f, 0.f, 1.f);
Vec3<float> H = L + state.viewDir;
float specular_factor = Dot33(H.NormalizedOr001(useSSE4), worldnormal);
specular_factor = pspLightPow(specular_factor, state.specularExp);
+1
View File
@@ -53,6 +53,7 @@ struct State {
Vec4<int> baseAmbientColorFactor;
float specularExp;
Vec3f viewDir;
struct {
bool colorForAmbient : 1;
+2
View File
@@ -28,6 +28,7 @@
#include "GPU/Common/DrawEngineCommon.h"
#include "GPU/Common/VertexDecoderCommon.h"
#include "GPU/Common/SoftwareTransformCommon.h"
#include "GPU/Common/TransformCommon.h"
#include "GPU/Common/VertexReader.h"
#include "GPU/GPUStateSIMDUtil.h"
#include "Common/Math/SIMDHeaders.h"
@@ -259,6 +260,7 @@ void ComputeTransformState(TransformState *state, const VertexReader &vreader) {
} else {
state->lightingState.usesWorldNormal = state->uvGenMode == GE_TEXMAP_ENVIRONMENT_MAP;
}
state->lightingState.viewDir = PSPViewDirection(gstate.viewMatrix);
float world[16];
float view[16];