From 2e37ca100119992cb5e4021d0c9693a2d041c3dc Mon Sep 17 00:00:00 2001 From: jessikitty Date: Fri, 24 Jul 2026 15:43:17 +1000 Subject: [PATCH] =?UTF-8?q?Fix:=20bind=20sampler=20uniforms=20to=20a=201x1?= =?UTF-8?q?=20transparent=20texture=20=E2=80=94=20null=20samplers=20broke?= =?UTF-8?q?=20shader=20linking,=20so=20no=20ghosts=20rendered?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/js/ghosts/loader.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/public/js/ghosts/loader.js b/public/js/ghosts/loader.js index 240a5d3..da7fa14 100644 --- a/public/js/ghosts/loader.js +++ b/public/js/ghosts/loader.js @@ -20,10 +20,20 @@ const objCache = new Map(); const texLoader = new THREE.TextureLoader(); const texCache = new Map(); +/* A sampler2D uniform must ALWAYS be bound to a real texture: leaving it null makes + * the shader program fail to link on many GPUs, which would break every untextured + * ghost. This 1x1 fully-transparent pixel is the safe no-op binding. */ +const EMPTY_TEX = (() => { + const t = new THREE.DataTexture(new Uint8Array([0, 0, 0, 0]), 1, 1, THREE.RGBAFormat); + t.needsUpdate = true; + return t; +})(); + function loadTexture(url) { if (!url) return null; if (!texCache.has(url)) { - const t = texLoader.load(url); + const t = texLoader.load(url, undefined, undefined, + () => console.warn('texture failed to load:', url)); // keeps the ghost alive t.colorSpace = THREE.SRGBColorSpace; texCache.set(url, t); } @@ -41,8 +51,8 @@ function gradientMaterial(top, bottom, opacity, faceTex, torsoTex, bands) { uMinY: { value: 0 }, uMaxY: { value: 1 }, uTime: { value: 0 }, - faceMap: { value: faceTex || null }, - torsoMap: { value: torsoTex || null }, + faceMap: { value: faceTex || EMPTY_TEX }, + torsoMap: { value: torsoTex || EMPTY_TEX }, hasFace: { value: faceTex ? 1 : 0 }, hasTorso: { value: torsoTex ? 1 : 0 }, // [centreY, halfHeight, halfWidth] in normalized model space @@ -136,7 +146,8 @@ function pickModel(manifest, ov) { export async function buildGhost(ghost, gradients, manifest, characters) { const ov = resolveOverrides(ghost, characters); - const grad = gradients[ghost.color] || gradients.Blue; + const grad = (gradients && (gradients[ghost.color] || gradients.Blue)) + || { top: '#51eaf1', bottom: '#529eff' }; // survive a missing/!oddly-shaped gradient payload const top = ov.topColor || grad.top; const bottom = ov.bottomColor || grad.bottom; const opacity = ov.opacity != null ? Number(ov.opacity) : 0.92;