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() {
// 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);
}
}
}

View File

@@ -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;