From b3584f41fdc3d7924988a34b7d4dcba5f7548de2 Mon Sep 17 00:00:00 2001 From: Sergej Lopatkin Date: Sun, 22 Feb 2026 21:50:55 +0100 Subject: [PATCH 1/2] wip: first draft of shadows casted by spotlight --- src/components/CalibrationPanel.vue | 40 +++++++++++++++++++++++++++-- src/composables/useThreeScene.ts | 29 ++++++++++++++------- src/data/defaults.ts | 10 +++++--- src/three/buildBox.ts | 14 ++++++---- src/three/buildCard.ts | 6 ++++- src/types/index.ts | 4 +++ 6 files changed, 83 insertions(+), 20 deletions(-) diff --git a/src/components/CalibrationPanel.vue b/src/components/CalibrationPanel.vue index 4cc62f8..d14f3a1 100644 --- a/src/components/CalibrationPanel.vue +++ b/src/components/CalibrationPanel.vue @@ -157,7 +157,7 @@ function formatValue(v: number): string {
Backlight
- {{ formatValue(store.config.lights.backlightIntensity) }} @@ -166,12 +166,48 @@ function formatValue(v: number): string {
Spotlight
- {{ formatValue(store.config.lights.spotlightIntensity) }}
+
+ Spot X +
+ + {{ formatValue(store.config.lights.spotlightX) }} +
+
+
+ Spot Y +
+ + {{ formatValue(store.config.lights.spotlightY) }} +
+
+
+ Spot angle +
+ + {{ formatValue(store.config.lights.spotlightAngle) }} +
+
+
+ Spot soft +
+ + {{ formatValue(store.config.lights.spotlightPenumbra) }} +
+
diff --git a/src/composables/useThreeScene.ts b/src/composables/useThreeScene.ts index bc0081d..929482c 100644 --- a/src/composables/useThreeScene.ts +++ b/src/composables/useThreeScene.ts @@ -406,13 +406,8 @@ export function useThreeScene(containerRef: Ref) { // Update off-axis camera updateOffAxisCamera(store.eyePos.x, store.eyePos.y, store.eyePos.z) - // Update head-tracked spotlight position + // Spotlight (fixed position — intensity updated below) const spotlight = scene.getObjectByName('headSpotlight') as SpotLight | undefined - if (spotlight) { - spotlight.position.set(store.eyePos.x * 0.8, store.eyePos.y * 0.8, store.eyePos.z * 0.6) - spotlight.target.position.set(0, 0, -store.dimensions.boxD * 0.5) - spotlight.target.updateMatrixWorld() - } // Update tilt springs (gyroscope or mouse) const tilt = gyroscope.isActive.value ? gyroscope : mouseTilt @@ -533,10 +528,26 @@ export function useThreeScene(containerRef: Ref) { back.intensity += (targetB - back.intensity) * dimRate } - // Update spotlight intensity from config + // Update spotlight from config if (spotlight) { - spotlight.intensity += - (store.config.lights.spotlightIntensity - spotlight.intensity) * dimRate + const targetSpot = store.isDimmed ? 0 : store.config.lights.spotlightIntensity + spotlight.intensity += (targetSpot - spotlight.intensity) * dimRate + const { screenW, screenH, boxD } = store.dimensions + const spotX = store.config.lights.spotlightX + const spotY = store.config.lights.spotlightY + spotlight.position.set( + (screenW / 2) * spotX, + (screenH / 2) * spotY, + boxD * 0.1, + ) + spotlight.target.position.set( + -(screenW / 2) * spotX * 0.4, + -(screenH / 2) * spotY * 0.2, + -boxD * 0.6, + ) + spotlight.target.updateMatrixWorld() + spotlight.angle = (store.config.lights.spotlightAngle * Math.PI) / 180 + spotlight.penumbra = store.config.lights.spotlightPenumbra } renderer.render(scene, camera) diff --git a/src/data/defaults.ts b/src/data/defaults.ts index 883b5ba..c72c8f4 100644 --- a/src/data/defaults.ts +++ b/src/data/defaults.ts @@ -16,10 +16,14 @@ export const DEFAULT_CONFIG: AppConfig = { webcamOffsetX: 0, webcamOffsetY: 0, lights: { - ambientIntensity: 0.4, - directionalIntensity: 2.4, - backlightIntensity: 1.6, + ambientIntensity: 0.2, + directionalIntensity: 1.9, + backlightIntensity: 10.0, spotlightIntensity: 1.4, + spotlightX: 0.8, + spotlightY: 0.8, + spotlightAngle: 48, + spotlightPenumbra: 1.0, }, shaders: { illustrationRare: { diff --git a/src/three/buildBox.ts b/src/three/buildBox.ts index c8715a8..905d432 100644 --- a/src/three/buildBox.ts +++ b/src/three/buildBox.ts @@ -317,18 +317,22 @@ export function buildBoxShell( scene.add(new AmbientLight(0xffffff, 0.05)) } - // Head-tracked spotlight for dynamic shadows + // Fixed spotlight casting shadows onto the walls + const spotX = lights?.spotlightX ?? 0.8 + const spotY = lights?.spotlightY ?? 0.9 + const spotAngle = lights?.spotlightAngle ?? 45 + const spotPenumbra = lights?.spotlightPenumbra ?? 0.5 const spotI = lights?.spotlightIntensity ?? 0.6 - const spotlight = new SpotLight(0xffffff, spotI, boxD * 3, Math.PI / 5, 0.4, 1.5) + const spotlight = new SpotLight(0xffffff, spotI, 0, (spotAngle * Math.PI) / 180, spotPenumbra, 0) spotlight.name = 'headSpotlight' spotlight.castShadow = true spotlight.shadow.mapSize.width = 1024 spotlight.shadow.mapSize.height = 1024 spotlight.shadow.camera.near = 5 - spotlight.shadow.camera.far = boxD * 3 + spotlight.shadow.camera.far = dims.eyeDefaultZ + boxD spotlight.shadow.bias = -0.001 - spotlight.position.set(0, 0, dims.eyeDefaultZ * 0.5) - spotlight.target.position.set(0, 0, -boxD / 2) + spotlight.position.set(hw * spotX, hh * spotY, boxD * 0.1) + spotlight.target.position.set(-hw * spotX * 0.4, -hh * spotY * 0.2, -boxD * 0.6) scene.add(spotlight) scene.add(spotlight.target) } diff --git a/src/three/buildCard.ts b/src/three/buildCard.ts index 213b554..354b6a7 100644 --- a/src/three/buildCard.ts +++ b/src/three/buildCard.ts @@ -3,7 +3,9 @@ import { DoubleSide, Mesh, MeshBasicMaterial, + MeshDepthMaterial, PlaneGeometry, + RGBADepthPacking, RGBAFormat, ShaderMaterial, UnsignedByteType, @@ -152,7 +154,7 @@ export function buildCardMesh( uniforms.uBirthdayDank2Tex = { value: birthdayTextures?.dank2 ?? blackPixel } } - return new Mesh( + const mesh = new Mesh( cardGeo, new ShaderMaterial({ uniforms, @@ -162,6 +164,8 @@ export function buildCardMesh( transparent: true, }), ) + mesh.customDepthMaterial = new MeshDepthMaterial({ depthPacking: RGBADepthPacking }) + return mesh } export function buildActivationMaterial( diff --git a/src/types/index.ts b/src/types/index.ts index bc1fe71..ada51ba 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -3,6 +3,10 @@ export interface LightConfig { directionalIntensity: number backlightIntensity: number spotlightIntensity: number + spotlightX: number + spotlightY: number + spotlightAngle: number + spotlightPenumbra: number } export interface SceneConfig { From 4a76f3526fcb9ca5f1cb97b8041bd56b3be7f085 Mon Sep 17 00:00:00 2001 From: Sergej Lopatkin Date: Sun, 22 Feb 2026 22:13:14 +0100 Subject: [PATCH 2/2] feat: dim scene with candle lighting Adds subtle candle lighting to the scene to improve the ambiance. Improves shadow quality and adjusts default light intensities for a more balanced visual experience. --- src/composables/useThreeScene.ts | 34 +++++++++++++++++++ src/data/defaults.ts | 10 +++--- src/three/buildBox.ts | 56 +++++++++++++++++++++++++++++--- 3 files changed, 91 insertions(+), 9 deletions(-) diff --git a/src/composables/useThreeScene.ts b/src/composables/useThreeScene.ts index 929482c..b32c0ef 100644 --- a/src/composables/useThreeScene.ts +++ b/src/composables/useThreeScene.ts @@ -44,6 +44,8 @@ export function useThreeScene(containerRef: Ref) { let camera: PerspectiveCamera | null = null let animationId: number | null = null let lastTime = performance.now() * 0.001 + let dimStartTime = 0 + let wasDimmed = false // Card state const cardMeshes = shallowRef([]) @@ -528,6 +530,38 @@ export function useThreeScene(containerRef: Ref) { back.intensity += (targetB - back.intensity) * dimRate } + // Track dim transition for candle stagger timing + if (store.isDimmed && !wasDimmed) dimStartTime = time + wasDimmed = store.isDimmed + + // Animate candle lights + flames (staggered on, together off, with flicker) + for (let i = 0; i < 5; i++) { + const candle = scene.getObjectByName(`candle${i}`) as PointLight | undefined + const flame = scene.getObjectByName(`candleFlame${i}`) as Mesh | undefined + if (!candle) continue + if (store.isDimmed) { + const elapsed = time - dimStartTime + const delay = i * 0.3 + const baseTarget = elapsed > delay ? 0.8 : 0 + const flicker = baseTarget * (0.9 + 0.1 * Math.sin(time * (3 + i * 0.7))) + candle.intensity += (flicker - candle.intensity) * dimRate + // Flame visibility + wobble + if (flame) { + const targetOpacity = baseTarget > 0 ? 0.9 : 0 + const mat = flame.material as { opacity: number } + mat.opacity += (targetOpacity - mat.opacity) * dimRate + const scaleFlicker = 0.85 + 0.15 * Math.sin(time * (4 + i * 1.1)) + flame.scale.set(scaleFlicker, 0.9 + 0.2 * Math.sin(time * (3.5 + i * 0.9)), scaleFlicker) + } + } else { + candle.intensity += (0 - candle.intensity) * dimRate + if (flame) { + const mat = flame.material as { opacity: number } + mat.opacity += (0 - mat.opacity) * dimRate + } + } + } + // Update spotlight from config if (spotlight) { const targetSpot = store.isDimmed ? 0 : store.config.lights.spotlightIntensity diff --git a/src/data/defaults.ts b/src/data/defaults.ts index c72c8f4..c204047 100644 --- a/src/data/defaults.ts +++ b/src/data/defaults.ts @@ -16,12 +16,12 @@ export const DEFAULT_CONFIG: AppConfig = { webcamOffsetX: 0, webcamOffsetY: 0, lights: { - ambientIntensity: 0.2, - directionalIntensity: 1.9, + ambientIntensity: 0.1, + directionalIntensity: 1.3, backlightIntensity: 10.0, - spotlightIntensity: 1.4, - spotlightX: 0.8, - spotlightY: 0.8, + spotlightIntensity: 1.3, + spotlightX: 0.4, + spotlightY: 1.0, spotlightAngle: 48, spotlightPenumbra: 1.0, }, diff --git a/src/three/buildBox.ts b/src/three/buildBox.ts index 905d432..db5c51b 100644 --- a/src/three/buildBox.ts +++ b/src/three/buildBox.ts @@ -1,7 +1,10 @@ import { AmbientLight, BufferGeometry, + ConeGeometry, + CylinderGeometry, DirectionalLight, + MeshBasicMaterial, Line, LineDashedMaterial, Mesh, @@ -313,6 +316,50 @@ export function buildBoxShell( backLight.name = 'solidBack' backLight.position.set(0, 0, -boxD * 0.7) scene.add(backLight) + + // Candle objects + lights along the bottom of the back wall + const candleSpacing = [-0.6, -0.3, 0, 0.3, 0.6] + const candleH = hh * 0.08 + const candleR = hh * 0.012 + const candleMat = new MeshStandardMaterial({ + color: 0xf5f0e0, + emissive: 0x222018, + emissiveIntensity: 0.2, + roughness: 0.8, + }) + for (let i = 0; i < candleSpacing.length; i++) { + const cx = hw * candleSpacing[i]! + const cy = -hh + candleH / 2 + const cz = -boxD * 0.85 + + // Candle body + const body = new Mesh(new CylinderGeometry(candleR, candleR, candleH, 8), candleMat) + body.position.set(cx, cy, cz) + body.receiveShadow = true + scene.add(body) + + // Flame (cone, transparent — faded in/out with candle light) + const flameH = candleH * 0.5 + const flameR = candleR * 0.6 + const flame = new Mesh( + new ConeGeometry(flameR, flameH, 6), + new MeshBasicMaterial({ + color: 0xffcc44, + transparent: true, + opacity: 0, + depthWrite: false, + }), + ) + flame.name = `candleFlame${i}` + flame.position.set(cx, -hh + candleH + flameH / 2, cz) + scene.add(flame) + + // Point light (off by default, animated in dim mode) + const light = new PointLight(0xffaa44, 0, boxD * 0.6, 0) + light.name = `candle${i}` + light.position.set(cx, -hh + candleH + flameH * 0.4, cz) + scene.add(light) + } } else { scene.add(new AmbientLight(0xffffff, 0.05)) } @@ -320,14 +367,15 @@ export function buildBoxShell( // Fixed spotlight casting shadows onto the walls const spotX = lights?.spotlightX ?? 0.8 const spotY = lights?.spotlightY ?? 0.9 - const spotAngle = lights?.spotlightAngle ?? 45 - const spotPenumbra = lights?.spotlightPenumbra ?? 0.5 + const spotAngle = lights?.spotlightAngle ?? 48 + const spotPenumbra = lights?.spotlightPenumbra ?? 1.0 const spotI = lights?.spotlightIntensity ?? 0.6 const spotlight = new SpotLight(0xffffff, spotI, 0, (spotAngle * Math.PI) / 180, spotPenumbra, 0) spotlight.name = 'headSpotlight' spotlight.castShadow = true - spotlight.shadow.mapSize.width = 1024 - spotlight.shadow.mapSize.height = 1024 + spotlight.shadow.mapSize.width = 4096 + spotlight.shadow.mapSize.height = 4096 + spotlight.shadow.radius = 10 spotlight.shadow.camera.near = 5 spotlight.shadow.camera.far = dims.eyeDefaultZ + boxD spotlight.shadow.bias = -0.001