Back to Supersonic RC Revive
SOURCE / PINNED RELEASE

Made of little things.

Supersonic RC Revive

Release
1ba42f1ca1d6…
Author-recorded commit
baecad10b1cd…
License
LICENSE
Author’s source reference
nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.napplet.soy%2F/n-143146b0d6f

Archive hash verified: 90d22b206672eba4…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/game/render/materials.ts
// An approximation of Shockwave 3D's #standard shader: Lambert lighting from
// the level's directional + ambient lights, base texture (or diffuse colour),
// then texture layer 2 multiplied in (lightMapManager's lightmaps).
import { Color, DoubleSide, Matrix3, ShaderMaterial, type Texture, Vector3 } from 'three';

export interface DirectorLight {
  direction: Vector3; // direction the light travels (node -Z)
  color: Color;
}

export interface LightRig {
  ambient: Color;
  directional: DirectorLight[];
}

/**
 * The level's authored lights (W3D 0x50 resources + 0x71 nodes): direction is
 * each node's -Z axis. The resources carry an extra intensity value (0.167,
 * Direct05 0.5) that Director's light objects do not expose; colours are white.
 */
export function levelLights(useIntensity: boolean): LightRig {
  const i = (v: number) => (useIntensity ? v : 1);
  const white = (v: number) => new Color(i(v), i(v), i(v));
  return {
    ambient: new Color(0.078, 0.11, 0.141),
    directional: [
      { direction: new Vector3(0, -0.771, -0.637), color: white(0.167) }, // Direct02
      { direction: new Vector3(0.891, 0.363, -0.273), color: white(0.167) }, // Direct04
      { direction: new Vector3(0, 0, 1), color: white(0.5) }, // Direct05 (from below)
      { direction: new Vector3(-0.926, -0.176, -0.334), color: white(0.167) }, // Direct06
      { direction: new Vector3(-0.915, 0.207, -0.346), color: white(0.167) }, // Direct07
      { direction: new Vector3(0.94, -0.185, -0.288), color: white(0.167) }, // Direct08
    ],
  };
}

/**
 * Whether the W3D light intensities apply. Compared side by side with the
 * original under Wine, full-strength white lights match its floor and walls.
 */
export const LIGHT_SETTINGS = { useIntensity: false };

/** arrowcam's rootNode is the arrow itself, so no scene light reaches it. */
export const UNLIT: LightRig = { ambient: new Color(0, 0, 0), directional: [] };
export let LEVEL_LIGHTS: LightRig = levelLights(LIGHT_SETTINGS.useIntensity);

export interface StandardShaderOptions {
  diffuse: Color;
  map: Texture | null;
  lightMap: Texture | null;
  opacity?: number;
  alphaTest?: number;
  lights?: LightRig;
  /** Scale applied to the lightmap multiply (Director multiplies 1:1). */
  lightMapGain?: number;
  emissive?: Color;
  /** Material ambient reflectance (times the scene ambient light). */
  ambient?: Color;
  doubleSided?: boolean;
  /** Light comes from the geometry's baked `bake` attribute instead of the rig. */
  baked?: boolean;
}

const vertexShader = /* glsl */ `
  attribute vec2 uv1;
  attribute vec4 bake;
  uniform mat3 mapTransform;
  varying vec2 vUv;
  varying vec2 vUv1;
  varying vec3 vNormal;
  varying vec3 vBake;
  void main() {
    vUv = (mapTransform * vec3(uv, 1.0)).xy;
    vUv1 = uv1;
    vBake = bake.rgb;
    vNormal = normalize(mat3(modelMatrix) * normal);
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`;

const fragmentShader = /* glsl */ `
  uniform vec3 diffuse;
  uniform vec3 emissive;
  uniform vec3 ambient;
  uniform float opacity;
  uniform float alphaTest;
  uniform bool hasMap;
  uniform bool hasLightMap;
  uniform bool baked;
  uniform sampler2D map;
  uniform sampler2D lightMap;
  uniform float lightMapGain;
  uniform vec3 ambientLight;
  uniform vec3 lightDir[6];
  uniform vec3 lightColor[6];
  varying vec2 vUv;
  varying vec2 vUv1;
  varying vec3 vNormal;
  varying vec3 vBake;
  void main() {
    vec4 base = hasMap ? texture2D(map, vUv) : vec4(1.0);
    if (base.a < alphaTest) discard;
    vec3 n = normalize(vNormal) * (gl_FrontFacing ? 1.0 : -1.0);
    vec3 lit = ambient * ambientLight;
    for (int i = 0; i < 6; i++) lit += lightColor[i] * max(dot(n, -lightDir[i]), 0.0);
    vec3 color = base.rgb * diffuse * (baked ? vBake : min(lit, vec3(1.0))) + emissive;
    if (hasLightMap) color *= texture2D(lightMap, vUv1).rgb * lightMapGain;
    gl_FragColor = vec4(color, base.a * opacity);
    #include <colorspace_fragment>
  }
`;

export function standardShader(o: StandardShaderOptions): ShaderMaterial {
  const lights = o.lights ?? LEVEL_LIGHTS;
  const dirs = lights.directional.map((l) => l.direction.clone().normalize());
  const cols = lights.directional.map((l) => new Vector3(l.color.r, l.color.g, l.color.b));
  while (dirs.length < 6) {
    dirs.push(new Vector3(0, 0, -1));
    cols.push(new Vector3());
  }
  const material = new ShaderMaterial({
    vertexShader,
    fragmentShader,
    uniforms: {
      diffuse: { value: o.diffuse },
      emissive: { value: o.emissive ?? new Color(0, 0, 0) },
      ambient: { value: o.ambient ?? new Color(1, 1, 1) },
      opacity: { value: o.opacity ?? 1 },
      alphaTest: { value: o.alphaTest ?? 0 },
      hasMap: { value: !!o.map },
      hasLightMap: { value: !!o.lightMap },
      baked: { value: !!o.baked },
      map: { value: o.map },
      // KHR_texture_transform tiling from editor levels; identity for everything else.
      mapTransform: { value: o.map ? o.map.matrix : new Matrix3() },
      lightMap: { value: o.lightMap },
      lightMapGain: { value: o.lightMapGain ?? 1 },
      ambientLight: { value: new Vector3(lights.ambient.r, lights.ambient.g, lights.ambient.b) },
      lightDir: { value: dirs.slice(0, 6) },
      lightColor: { value: cols.slice(0, 6) },
    },
    transparent: (o.opacity ?? 1) < 1,
    side: o.doubleSided === false ? undefined : DoubleSide,
  });
  return material;
}