feat: Add a visual flashlight model with a secondary point light and enhance volumetric dust particle lighting and performance.

This commit is contained in:
2026-01-03 08:55:31 +00:00
parent 78c02169ef
commit 2eac31aae9
2 changed files with 83 additions and 31 deletions

View File

@@ -54,16 +54,54 @@ export class Player {
} }
setupFlashlight() { 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 = new THREE.SpotLight(0xffffff, 10);
this.flashlight.angle = Math.PI / 6; this.flashlight.angle = Math.PI / 6;
this.flashlight.penumbra = 0.3; this.flashlight.penumbra = 0.3;
this.flashlight.decay = 1.5; // Lower decay for further reach this.flashlight.decay = 1.5;
this.flashlight.distance = 60; // Significantly increased range 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.flashlightGroup.add(this.flashlight);
this.flashlight.position.set(0, 0, 0); this.flashlightGroup.add(this.flashlight.target);
this.flashlight.target.position.set(0, 0, -1);
this.camera.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() { setupInput() {
@@ -103,8 +141,6 @@ export class Player {
toggleFlashlight() { toggleFlashlight() {
if (!this.controls) return; 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.controls.isLocked) return;
if (this.battery <= 0 && this.flashlightOn === false) { if (this.battery <= 0 && this.flashlightOn === false) {
@@ -113,9 +149,10 @@ export class Player {
} }
this.flashlightOn = !this.flashlightOn; this.flashlightOn = !this.flashlightOn;
if (this.flashlight) { // Update all light components
this.flashlight.visible = this.flashlightOn; 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() { lockControls() {
@@ -219,6 +256,8 @@ export class Player {
if (this.battery <= 0) { if (this.battery <= 0) {
this.flashlightOn = false; this.flashlightOn = false;
this.flashlight.visible = 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!'); window.log('Battery depleted!');
} }
@@ -235,13 +274,16 @@ export class Player {
this.flashlight.intensity = Math.min(50, this.flashlight.intensity + speed * 10); this.flashlight.intensity = Math.min(50, this.flashlight.intensity + speed * 10);
this.flashlight.angle = Math.min(Math.PI / 2, this.flashlight.angle + angleSpeed); 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 // Log occasionally for feedback
if (Math.random() < 0.1) window.log(`Light: Int=${this.flashlight.intensity.toFixed(1)} Ang=${this.flashlight.angle.toFixed(2)}`); if (Math.random() < 0.1) window.log(`Light: Int=${this.flashlight.intensity.toFixed(1)} Ang=${this.flashlight.angle.toFixed(2)}`);
} else { } else {
// Only flicker if not adjusting? Or flicker on top of base intensity. // Flicker logic
// Let's modify base intensity and add flicker const flicker = (Math.random() - 0.5) * 0.5;
// Simplified: just flicker around the current value this.flashlight.intensity += flicker;
this.flashlight.intensity += (Math.random() - 0.5) * 0.5; this.bulbLight.intensity = Math.max(0, this.flashlight.intensity * 0.2);
} }
} }
} }

View File

@@ -48,8 +48,8 @@ export class World {
} }
createDust() { createDust() {
// Create 2000 dust particles // Create 800 dust particles (Reduced)
const count = 2000; const count = 800;
const geometry = new THREE.BufferGeometry(); const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(count * 3); const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3); // For controlling opacity/brightness per particle const colors = new Float32Array(count * 3); // For controlling opacity/brightness per particle
@@ -117,11 +117,16 @@ export class World {
// Flashlight info // Flashlight info
let lightPos, lightDir, lightAngle, lightDist, isLightOn; let lightPos, lightDir, lightAngle, lightDist, isLightOn;
if (player && player.flashlight) { if (player && player.flashlight) {
// Get world position and direction of camera/flashlight // Get world position and direction of FLASHLIGHT
lightPos = new THREE.Vector3(); lightPos = new THREE.Vector3();
lightDir = new THREE.Vector3(); lightDir = new THREE.Vector3();
player.camera.getWorldPosition(lightPos); const targetPos = new THREE.Vector3();
player.camera.getWorldDirection(lightDir);
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 lightAngle = player.flashlight.angle; // Cone half-angle
lightDist = player.flashlight.distance; 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]) > 20) positions[i * 3] *= -0.9;
if (Math.abs(positions[i * 3 + 2]) > 20) positions[i * 3 + 2] *= -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; let brightness = 0;
if (isLightOn) { if (isLightOn) {
// Check distance // Check distance
@@ -152,25 +157,30 @@ export class World {
const dist = pPos.distanceTo(lightPos); const dist = pPos.distanceTo(lightPos);
if (dist < lightDist) { if (dist < lightDist) {
// Check angle
// Vector from light to particle // Vector from light to particle
const toPart = pPos.sub(lightPos).normalize(); const toPart = pPos.sub(lightPos).normalize();
const angle = toPart.angleTo(lightDir); const angle = toPart.angleTo(lightDir);
// Strictly inside the cone with soft edges
if (angle < lightAngle) { if (angle < lightAngle) {
// Inside cone! // "Point of Light" Effect:
// Fade out at edges of cone? optional // 1. Radial falloff: Brightest in center of beam, fades to edge
// Fade out with distance const radialFactor = 1.0 - (angle / lightAngle);
const atten = 1.0 - (dist / lightDist);
brightness = atten * atten; // clearer near camera // 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) // Apply color (Gray * brightness)
colors[i * 3] = brightness; const grayScale = 0.3; // Make it gray/subtle
colors[i * 3 + 1] = brightness; colors[i * 3] = brightness * grayScale;
colors[i * 3 + 2] = brightness; colors[i * 3 + 1] = brightness * grayScale;
colors[i * 3 + 2] = brightness * grayScale;
} }
this.dustParticles.geometry.attributes.position.needsUpdate = true; this.dustParticles.geometry.attributes.position.needsUpdate = true;