t2-mapper/src/interiorMaterial.ts

158 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Vector3 } from "three";
/**
* Interior material shader modifications for MeshLambertMaterial.
*
* Matches Torque's rendering formula (sceneLighting.cc + interiorRender.cc):
* output = clamp(lighting × texture, 0, 1)
*
* Where:
* - Outside surfaces: lighting = clamp(clamp(scene_lighting) + lightmap)
* - Inside surfaces: lighting = lightmap
* - scene_lighting = sun_color × N·L + ambient_color
* - All operations in sRGB/gamma space
*
* Key insights from Torque source:
* 1. Scene lighting is clamped to [0,1] BEFORE adding to lightmap (line 1785)
* 2. The sum is clamped per-channel to [0,1] (lines 1817-1827)
* 3. Mission sun/ambient colors ARE sRGB values - Torque used them directly
* in gamma space math. When we pass them to Three.js (which interprets them
* as linear), the numerical values are preserved, so extracted lighting
* gives us the sRGB values we need - NO conversion required.
*
* We only convert:
* - Texture: linearToSRGB (Three.js decoded from sRGB)
* - Lightmap: linearToSRGB (Three.js decoded from sRGB)
* - Result: sRGBToLinear (Three.js expects linear output)
*/
export type InteriorLightingOptions = {
surfaceOutsideVisible?: boolean;
};
// sRGB <-> Linear conversion functions (GLSL)
const colorSpaceFunctions = /* glsl */ `
vec3 interiorLinearToSRGB(vec3 linear) {
vec3 higher = pow(linear, vec3(1.0/2.4)) * 1.055 - 0.055;
vec3 lower = linear * 12.92;
return mix(lower, higher, step(vec3(0.0031308), linear));
}
vec3 interiorSRGBToLinear(vec3 srgb) {
vec3 higher = pow((srgb + 0.055) / 1.055, vec3(2.4));
vec3 lower = srgb / 12.92;
return mix(lower, higher, step(vec3(0.04045), srgb));
}
// Debug grid overlay function using screen-space derivatives for sharp, anti-aliased lines
// Returns 1.0 on grid lines, 0.0 elsewhere
float debugGrid(vec2 uv, float gridSize, float lineWidth) {
vec2 scaledUV = uv * gridSize;
vec2 grid = abs(fract(scaledUV - 0.5) - 0.5) / fwidth(scaledUV);
float line = min(grid.x, grid.y);
return 1.0 - min(line / lineWidth, 1.0);
}
`;
export function injectInteriorLighting(
shader: any,
options: InteriorLightingOptions,
): void {
const isOutsideVisible = options.surfaceOutsideVisible ?? false;
// Outside surfaces: scene lighting + lightmap
// Inside surfaces: lightmap only (no scene lighting)
shader.uniforms.useSceneLighting = { value: isOutsideVisible };
// Debug color: blue for outside visible, red for inside
// Only used when DEBUG_MODE define is set
shader.uniforms.interiorDebugColor = {
value: isOutsideVisible
? new Vector3(0.0, 0.4, 1.0)
: new Vector3(1.0, 0.2, 0.0),
};
// Add color space functions and uniform
shader.fragmentShader = shader.fragmentShader.replace(
"#include <common>",
`#include <common>
${colorSpaceFunctions}
uniform bool useSceneLighting;
uniform vec3 interiorDebugColor;
`,
);
// Disable default lightmap handling - we'll handle it in the output
// (MeshLambertMaterial doesn't use envmap/IBL, so we only need the lightmap texel)
shader.fragmentShader = shader.fragmentShader.replace(
"#include <lights_fragment_maps>",
`// Lightmap handled in custom output calculation
#ifdef USE_LIGHTMAP
vec4 lightMapTexel = texture2D( lightMap, vLightMapUv );
#endif`,
);
// Override outgoingLight with Torque-style gamma-space calculation
// Using #include <opaque_fragment> as a stable replacement target (it consumes outgoingLight)
shader.fragmentShader = shader.fragmentShader.replace(
"#include <opaque_fragment>",
`// Torque-style lighting: output = clamp(lighting × texture, 0, 1) in sRGB space
// Get texture in sRGB space (undo Three.js linear decode)
vec3 textureSRGB = interiorLinearToSRGB(diffuseColor.rgb);
// Compute lighting in sRGB space
vec3 lightingSRGB = vec3(0.0);
if (useSceneLighting) {
// Three.js computed: reflectedLight = lighting × texture_linear / PI
// Extract pure lighting: lighting = reflectedLight × PI / texture_linear
vec3 totalLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse;
vec3 safeTexLinear = max(diffuseColor.rgb, vec3(0.001));
vec3 extractedLighting = totalLight * PI / safeTexLinear;
// NOTE: extractedLighting is ALREADY sRGB values because mission sun/ambient colors
// are sRGB values (Torque used them directly in gamma space). Three.js treats them
// as linear but the numerical values are the same. DO NOT convert to sRGB here!
// IMPORTANT: Torque clamps scene lighting to [0,1] BEFORE adding to lightmap
// (sceneLighting.cc line 1785: tmp.clamp())
lightingSRGB = clamp(extractedLighting, 0.0, 1.0);
}
// Add lightmap contribution (for BOTH outside and inside surfaces)
// In Torque, scene lighting is ADDED to lightmaps for outside surfaces at mission load
// (stored in .ml files). Inside surfaces only have base lightmap. Both need lightmap here.
#ifdef USE_LIGHTMAP
// Lightmap is stored as linear in Three.js (decoded from sRGB texture), convert back
lightingSRGB += interiorLinearToSRGB(lightMapTexel.rgb);
#endif
// Torque clamps the sum to [0,1] per channel (sceneLighting.cc lines 1817-1827)
lightingSRGB = clamp(lightingSRGB, 0.0, 1.0);
// Torque formula: output = clamp(lighting × texture, 0, 1) in sRGB/gamma space
vec3 resultSRGB = clamp(lightingSRGB * textureSRGB, 0.0, 1.0);
// Convert back to linear for Three.js output pipeline
vec3 resultLinear = interiorSRGBToLinear(resultSRGB);
// Reassign outgoingLight before opaque_fragment consumes it
outgoingLight = resultLinear + totalEmissiveRadiance;
#include <opaque_fragment>`,
);
// Add debug grid overlay AFTER opaque_fragment sets gl_FragColor
// This ensures our debug visualization isn't affected by the Torque lighting calculations
shader.fragmentShader = shader.fragmentShader.replace(
"#include <tonemapping_fragment>",
`// Debug mode: overlay colored grid on top of normal rendering
// Blue grid = SurfaceOutsideVisible (receives scene ambient light)
// Red grid = inside surface (no scene ambient light)
#if DEBUG_MODE && defined(USE_MAP)
// gridSize=4 creates 4x4 grid per UV tile, lineWidth=1.5 is ~1.5 pixels wide
float gridIntensity = debugGrid(vMapUv, 4.0, 1.5);
gl_FragColor.rgb = mix(gl_FragColor.rgb, interiorDebugColor, gridIntensity * 0.1);
#endif
#include <tonemapping_fragment>`,
);
}