From 2eac31aae9bce3906c6342e561cfe4fc5dbaf97b Mon Sep 17 00:00:00 2001 From: eliazarw Date: Sat, 3 Jan 2026 08:55:31 +0000 Subject: [PATCH] feat: Add a visual flashlight model with a secondary point light and enhance volumetric dust particle lighting and performance. --- src/Player.js | 72 ++++++++++++++++++++++++++++++++++++++++----------- src/World.js | 42 ++++++++++++++++++------------ 2 files changed, 83 insertions(+), 31 deletions(-) diff --git a/src/Player.js b/src/Player.js index afb6fb6..0591378 100644 --- a/src/Player.js +++ b/src/Player.js @@ -54,16 +54,54 @@ export class Player { } setupFlashlight() { + // Group to hold the visual model and lights + this.flashlightGroup = new THREE.Group(); + this.camera.add(this.flashlightGroup); + + // Position: Bottom-right, slightly forward + this.flashlightGroup.position.set(0.3, -0.25, -0.4); + + // 1. Visual Model + // Flashlight Body + const bodyGeo = new THREE.CylinderGeometry(0.03, 0.04, 0.2, 16); + bodyGeo.rotateX(-Math.PI / 2); // Point forward + const bodyMat = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.5, metalness: 0.8 }); + const body = new THREE.Mesh(bodyGeo, bodyMat); + this.flashlightGroup.add(body); + + // Flashlight Bulb (Emmissive) + const bulbGeo = new THREE.CircleGeometry(0.025, 16); + const bulbMat = new THREE.MeshBasicMaterial({ color: 0xffffff }); + const bulb = new THREE.Mesh(bulbGeo, bulbMat); + bulb.position.z = -0.101; // Tip of body + this.flashlightGroup.add(bulb); + this.bulbMesh = bulb; // To toggle emission + + // Hand (Simple representation) + const handGeo = new THREE.BoxGeometry(0.08, 0.08, 0.15); + const handMat = new THREE.MeshStandardMaterial({ color: 0xdcb898 }); // Skin tone + const hand = new THREE.Mesh(handGeo, handMat); + hand.position.set(0.05, -0.05, 0.05); // Grip position + hand.rotation.set(0.2, 0.2, 0); + this.flashlightGroup.add(hand); + + // 2. Lights + // Main SpotLight (The beam) this.flashlight = new THREE.SpotLight(0xffffff, 10); this.flashlight.angle = Math.PI / 6; this.flashlight.penumbra = 0.3; - this.flashlight.decay = 1.5; // Lower decay for further reach - this.flashlight.distance = 60; // Significantly increased range + this.flashlight.decay = 1.5; + this.flashlight.distance = 60; + this.flashlight.position.set(0, 0, -0.1); // At tip + this.flashlight.target.position.set(0, 0, -10); // Aim forward - this.camera.add(this.flashlight); - this.flashlight.position.set(0, 0, 0); - this.flashlight.target.position.set(0, 0, -1); - this.camera.add(this.flashlight.target); + this.flashlightGroup.add(this.flashlight); + this.flashlightGroup.add(this.flashlight.target); + + // PointLight (The glow around the player) + this.bulbLight = new THREE.PointLight(0xffffff, 2, 4); // Low range (4m) + this.bulbLight.position.set(0, 0, -0.15); // Slightly ahead of tip + this.flashlightGroup.add(this.bulbLight); } setupInput() { @@ -103,8 +141,6 @@ export class Player { toggleFlashlight() { if (!this.controls) return; - // Note: isLocked might be false if user escaped, but we allow 'F' if controls exist? - // Better to allow F only when locked to avoid confusion. if (!this.controls.isLocked) return; if (this.battery <= 0 && this.flashlightOn === false) { @@ -113,9 +149,10 @@ export class Player { } this.flashlightOn = !this.flashlightOn; - if (this.flashlight) { - this.flashlight.visible = this.flashlightOn; - } + // Update all light components + if (this.flashlight) this.flashlight.visible = this.flashlightOn; + if (this.bulbLight) this.bulbLight.visible = this.flashlightOn; + if (this.bulbMesh) this.bulbMesh.material.color.setHex(this.flashlightOn ? 0xffffff : 0x111111); } lockControls() { @@ -219,6 +256,8 @@ export class Player { if (this.battery <= 0) { this.flashlightOn = false; this.flashlight.visible = false; + if (this.bulbLight) this.bulbLight.visible = false; + if (this.bulbMesh) this.bulbMesh.material.color.setHex(0x111111); window.log('Battery depleted!'); } @@ -235,13 +274,16 @@ export class Player { this.flashlight.intensity = Math.min(50, this.flashlight.intensity + speed * 10); this.flashlight.angle = Math.min(Math.PI / 2, this.flashlight.angle + angleSpeed); } + // Sync bulb light + this.bulbLight.intensity = this.flashlight.intensity * 0.2; + // Log occasionally for feedback if (Math.random() < 0.1) window.log(`Light: Int=${this.flashlight.intensity.toFixed(1)} Ang=${this.flashlight.angle.toFixed(2)}`); } else { - // Only flicker if not adjusting? Or flicker on top of base intensity. - // Let's modify base intensity and add flicker - // Simplified: just flicker around the current value - this.flashlight.intensity += (Math.random() - 0.5) * 0.5; + // Flicker logic + const flicker = (Math.random() - 0.5) * 0.5; + this.flashlight.intensity += flicker; + this.bulbLight.intensity = Math.max(0, this.flashlight.intensity * 0.2); } } } diff --git a/src/World.js b/src/World.js index c69b36d..a4387cd 100644 --- a/src/World.js +++ b/src/World.js @@ -48,8 +48,8 @@ export class World { } createDust() { - // Create 2000 dust particles - const count = 2000; + // Create 800 dust particles (Reduced) + const count = 800; const geometry = new THREE.BufferGeometry(); const positions = new Float32Array(count * 3); const colors = new Float32Array(count * 3); // For controlling opacity/brightness per particle @@ -117,11 +117,16 @@ export class World { // Flashlight info let lightPos, lightDir, lightAngle, lightDist, isLightOn; if (player && player.flashlight) { - // Get world position and direction of camera/flashlight + // Get world position and direction of FLASHLIGHT lightPos = new THREE.Vector3(); lightDir = new THREE.Vector3(); - player.camera.getWorldPosition(lightPos); - player.camera.getWorldDirection(lightDir); + const targetPos = new THREE.Vector3(); + + player.flashlight.getWorldPosition(lightPos); + player.flashlight.target.getWorldPosition(targetPos); + + // True Direction: Target - Position + lightDir.subVectors(targetPos, lightPos).normalize(); lightAngle = player.flashlight.angle; // Cone half-angle lightDist = player.flashlight.distance; @@ -144,7 +149,7 @@ export class World { if (Math.abs(positions[i * 3]) > 20) positions[i * 3] *= -0.9; if (Math.abs(positions[i * 3 + 2]) > 20) positions[i * 3 + 2] *= -0.9; - // 2. Lighting Check + // 2. Lighting Check (Volumetric Beam) let brightness = 0; if (isLightOn) { // Check distance @@ -152,25 +157,30 @@ export class World { const dist = pPos.distanceTo(lightPos); if (dist < lightDist) { - // Check angle // Vector from light to particle const toPart = pPos.sub(lightPos).normalize(); const angle = toPart.angleTo(lightDir); + // Strictly inside the cone with soft edges if (angle < lightAngle) { - // Inside cone! - // Fade out at edges of cone? optional - // Fade out with distance - const atten = 1.0 - (dist / lightDist); - brightness = atten * atten; // clearer near camera + // "Point of Light" Effect: + // 1. Radial falloff: Brightest in center of beam, fades to edge + const radialFactor = 1.0 - (angle / lightAngle); + + // 2. Distance falloff: Fades with distance + const distFactor = 1.0 - (dist / lightDist); + + // Combine: Power of 4 makes it look like a tight beam/point + brightness = Math.pow(radialFactor * distFactor, 4); } } } - // Apply color (White * brightness) - colors[i * 3] = brightness; - colors[i * 3 + 1] = brightness; - colors[i * 3 + 2] = brightness; + // Apply color (Gray * brightness) + const grayScale = 0.3; // Make it gray/subtle + colors[i * 3] = brightness * grayScale; + colors[i * 3 + 1] = brightness * grayScale; + colors[i * 3 + 2] = brightness * grayScale; } this.dustParticles.geometry.attributes.position.needsUpdate = true;