582 lines
24 KiB
HTML
582 lines
24 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Gloomy Room Shadow Scene with Bloom</title>
|
|
<!-- Load Tailwind CSS for utility styling --><script src="https://cdn.tailwindcss.com"></script>
|
|
<!-- Load Three.js library --><script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<!-- Load Three.js Post-processing modules --><script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/EffectComposer.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/RenderPass.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/ShaderPass.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/CopyShader.js"></script>
|
|
<!-- FIX: Added missing dependency for UnrealBloomPass -->
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/FXAAShader.js"></script>
|
|
<style>
|
|
/* Custom styles for the 3D canvas */
|
|
body {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
font-family: 'Inter', sans-serif;
|
|
background-color: #0d0d10; /* Very dark background */
|
|
}
|
|
canvas {
|
|
display: block;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="info" class="absolute top-4 left-1/2 transform -translate-x-1/2 p-2 bg-gray-900 bg-opacity-70 text-white rounded-lg shadow-xl text-xs sm:text-sm z-10">
|
|
Spiders and flies scattering in a gloomy, shadow-casting room with bloom. (Use mouse to look around)
|
|
</div>
|
|
|
|
<script>
|
|
// --- Global Variables ---
|
|
let scene, camera, renderer, composer;
|
|
let ambientLight, lampLight, lampMesh;
|
|
let pointer = new THREE.Vector2();
|
|
let targetRotation = { x: 0, y: 0 };
|
|
const BUGS_COUNT = 30; // Spiders
|
|
const FLIES_COUNT = 20; // Flies
|
|
const bugs = [];
|
|
const flies = [];
|
|
const floorHeight = 0;
|
|
let landingSurfaces = []; // Array to hold floor and table for fly landings
|
|
const raycaster = new THREE.Raycaster();
|
|
|
|
|
|
// --- Configuration ---
|
|
const ROOM_SIZE = 15;
|
|
const LAMP_HEIGHT = 4;
|
|
const BUG_SPEED = 0.007;
|
|
const FLIGHT_HEIGHT_MIN = 1.0; // Min height for flying
|
|
const FLIGHT_HEIGHT_MAX = 3.0; // Max height for flying
|
|
const FLY_FLIGHT_SPEED_FACTOR = 0.01; // How quickly 't' increases per frame
|
|
const DAMPING_FACTOR = 0.05;
|
|
|
|
// Bloom parameters
|
|
const bloomParams = {
|
|
exposure: 1,
|
|
bloomStrength: 1.5, // How intense the bloom is
|
|
bloomThreshold: 0.8, // Only pixels brighter than this will bloom
|
|
bloomRadius: 0.5 // How far the bloom spreads
|
|
};
|
|
|
|
// --- Utility Functions ---
|
|
|
|
/**
|
|
* Converts degrees to radians.
|
|
* @param {number} degrees
|
|
* @returns {number}
|
|
*/
|
|
function degToRad(degrees) {
|
|
return degrees * (Math.PI / 180);
|
|
}
|
|
|
|
/**
|
|
* Initializes the Three.js scene, camera, and renderer.
|
|
*/
|
|
function init() {
|
|
// 1. Scene Setup
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x101015);
|
|
|
|
// 2. Renderer Setup (Enable Shadows)
|
|
renderer = new THREE.WebGLRenderer({ antialias: true }); // Antialiasing is handled by FXAA now
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
renderer.shadowMap.enabled = true;
|
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Softer shadows
|
|
document.body.appendChild(renderer.domElement);
|
|
|
|
// 3. Camera Setup (First-person perspective near the floor)
|
|
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
camera.position.set(0, 1.5, 5); // Stand a bit above the floor, facing into the room
|
|
camera.lookAt(0, 1.5, 0);
|
|
|
|
// 4. Lighting Setup
|
|
setupLighting();
|
|
|
|
// 5. Environment Setup (Sets up landingSurfaces array)
|
|
setupRoom();
|
|
setupTable();
|
|
|
|
// 6. Creatures Setup
|
|
setupBugs(); // Spiders
|
|
setupFlies(); // Flies
|
|
|
|
// 7. Post-processing (Bloom) Setup
|
|
setupPostProcessing();
|
|
|
|
// 8. Event Listeners
|
|
window.addEventListener('resize', onWindowResize);
|
|
document.addEventListener('mousemove', onMouseMove);
|
|
|
|
console.log("Three.js Scene Initialized.");
|
|
animate();
|
|
}
|
|
|
|
/**
|
|
* Sets up the lighting: Ambient and the central PointLight (lamp).
|
|
*/
|
|
function setupLighting() {
|
|
// Very dim ambient light for general visibility
|
|
ambientLight = new THREE.AmbientLight(0x444444, 0.5);
|
|
scene.add(ambientLight);
|
|
|
|
// Point Light (The Lamp)
|
|
const lampColor = 0xffe9a0; // Warm, dim color
|
|
const lampIntensity = 1.8;
|
|
const lampDistance = 15;
|
|
|
|
lampLight = new THREE.PointLight(lampColor, lampIntensity, lampDistance);
|
|
lampLight.position.set(0, LAMP_HEIGHT, 0);
|
|
|
|
// Enable shadow casting for the lamp
|
|
lampLight.castShadow = true;
|
|
|
|
// Configure shadow camera properties for better shadow quality
|
|
lampLight.shadow.mapSize.width = 1024;
|
|
lampLight.shadow.mapSize.height = 1024;
|
|
lampLight.shadow.camera.near = 0.5;
|
|
lampLight.shadow.camera.far = 10;
|
|
lampLight.shadow.bias = -0.0001; // Tiny bias to prevent shadow artifacts
|
|
|
|
scene.add(lampLight);
|
|
|
|
// Visual mesh for the light source (the bulb)
|
|
const bulbGeometry = new THREE.SphereGeometry(0.1, 8, 8);
|
|
const bulbMaterial = new THREE.MeshBasicMaterial({ color: lampColor });
|
|
lampMesh = new THREE.Mesh(bulbGeometry, bulbMaterial);
|
|
lampMesh.position.copy(lampLight.position);
|
|
scene.add(lampMesh);
|
|
}
|
|
|
|
/**
|
|
* Sets up the room (walls and floor).
|
|
*/
|
|
function setupRoom() {
|
|
const wallMaterial = new THREE.MeshPhongMaterial({
|
|
color: 0x333333,
|
|
side: THREE.BackSide, // Render walls from the inside
|
|
shininess: 5,
|
|
});
|
|
|
|
// Floor
|
|
const floorGeometry = new THREE.PlaneGeometry(ROOM_SIZE * 2, ROOM_SIZE * 2);
|
|
const floorMesh = new THREE.Mesh(floorGeometry, wallMaterial);
|
|
floorMesh.rotation.x = degToRad(-90);
|
|
floorMesh.position.y = floorHeight;
|
|
floorMesh.receiveShadow = true;
|
|
floorMesh.name = 'landingSurface'; // Tag for fly landing
|
|
scene.add(floorMesh);
|
|
landingSurfaces.push(floorMesh); // Add to list of landing spots
|
|
|
|
// Walls (Simple box around the camera position)
|
|
const wallGeometry = new THREE.BoxGeometry(ROOM_SIZE * 2, ROOM_SIZE * 2, ROOM_SIZE * 2);
|
|
const wallsMesh = new THREE.Mesh(wallGeometry, wallMaterial);
|
|
wallsMesh.position.y = ROOM_SIZE; // Center the box vertically
|
|
wallsMesh.receiveShadow = true;
|
|
scene.add(wallsMesh);
|
|
}
|
|
|
|
/**
|
|
* Adds a simple table object for more complex shadows.
|
|
*/
|
|
function setupTable() {
|
|
const tableMaterial = new THREE.MeshPhongMaterial({
|
|
color: 0x5a3d2b, // Dark wood color
|
|
shininess: 10,
|
|
});
|
|
|
|
// Table top
|
|
const tableTop = new THREE.Mesh(
|
|
new THREE.BoxGeometry(3, 0.1, 1.5),
|
|
tableMaterial
|
|
);
|
|
tableTop.position.set(2, 1.5 + 0.05, 0);
|
|
tableTop.castShadow = true;
|
|
tableTop.receiveShadow = true;
|
|
tableTop.name = 'landingSurface'; // Tag for fly landing
|
|
scene.add(tableTop);
|
|
landingSurfaces.push(tableTop); // Add to list of landing spots
|
|
|
|
// Table legs (using a loop for simplicity)
|
|
const legGeometry = new THREE.BoxGeometry(0.1, 1.5, 0.1);
|
|
const positions = [
|
|
[3.4, 0.75, 0.65], [-0.4, 0.75, 0.65],
|
|
[3.4, 0.75, -0.65], [-0.4, 0.75, -0.65]
|
|
];
|
|
|
|
positions.forEach(pos => {
|
|
const leg = new THREE.Mesh(legGeometry, tableMaterial);
|
|
leg.position.set(pos[0], pos[1], pos[2]);
|
|
leg.castShadow = true;
|
|
leg.receiveShadow = true;
|
|
scene.add(leg);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Creates a single, more detailed spider mesh (sphere body and 8 legs).
|
|
* @returns {THREE.Group}
|
|
*/
|
|
function createBugMesh() {
|
|
const bugGroup = new THREE.Group();
|
|
|
|
// Material for all parts
|
|
const bugMaterial = new THREE.MeshPhongMaterial({
|
|
color: 0x0a0a0a, // Almost black
|
|
shininess: 30,
|
|
});
|
|
|
|
// --- 1. Body (Sphere) ---
|
|
const bodyRadius = 0.15;
|
|
const bodyGeometry = new THREE.SphereGeometry(bodyRadius, 16, 16);
|
|
const body = new THREE.Mesh(bodyGeometry, bugMaterial);
|
|
body.position.y = bodyRadius;
|
|
|
|
body.castShadow = true;
|
|
body.receiveShadow = true;
|
|
bugGroup.add(body);
|
|
|
|
// --- 2. Legs (8 thin cylinders) ---
|
|
const legLength = 0.3;
|
|
const legWidth = 0.005;
|
|
const legGeometry = new THREE.CylinderGeometry(legWidth, legWidth, legLength, 4);
|
|
const numLegs = 8;
|
|
const legAngleStart = degToRad(45 / 2);
|
|
const legRadialPivot = 0.08;
|
|
const legTiltDown = degToRad(15);
|
|
|
|
for (let i = 0; i < numLegs; i++) {
|
|
const leg = new THREE.Mesh(legGeometry, bugMaterial);
|
|
const angle = legAngleStart + (i * degToRad(360 / numLegs));
|
|
const pivotX = legRadialPivot * Math.sin(angle);
|
|
const pivotZ = legRadialPivot * Math.cos(angle);
|
|
leg.position.set(pivotX, bodyRadius - 0.03, pivotZ);
|
|
const halfLength = legLength / 2;
|
|
leg.position.x += halfLength * Math.sin(angle);
|
|
leg.position.z += halfLength * Math.cos(angle);
|
|
|
|
leg.rotation.x = degToRad(90);
|
|
leg.rotation.y = angle;
|
|
leg.rotation.z = (i % 2 === 0) ? legTiltDown : -legTiltDown;
|
|
|
|
leg.castShadow = true;
|
|
leg.receiveShadow = true;
|
|
bugGroup.add(leg);
|
|
}
|
|
|
|
bugGroup.position.y = floorHeight;
|
|
return bugGroup;
|
|
}
|
|
|
|
/**
|
|
* Creates a single fly mesh (small cone/tetrahedron).
|
|
* @returns {THREE.Group}
|
|
*/
|
|
function createFlyMesh() {
|
|
const flyGroup = new THREE.Group();
|
|
|
|
const flyMaterial = new THREE.MeshPhongMaterial({
|
|
color: 0x111111, // Dark fly color
|
|
shininess: 50,
|
|
});
|
|
|
|
// Small Cone/Tetrahedron for a simple shape
|
|
const bodyGeometry = new THREE.ConeGeometry(0.05, 0.1, 4);
|
|
const body = new THREE.Mesh(bodyGeometry, flyMaterial);
|
|
body.rotation.x = degToRad(90); // Point nose in Z direction
|
|
|
|
body.castShadow = true;
|
|
body.receiveShadow = true;
|
|
flyGroup.add(body);
|
|
|
|
// Initial state and parameters for the fly
|
|
flyGroup.userData = {
|
|
state: 'flying', // 'flying' or 'landed'
|
|
landTimer: 0,
|
|
t: 0, // Curve progression t parameter (0 to 1)
|
|
curve: null,
|
|
landCheckTimer: 0,
|
|
oscillationTime: Math.random() * 100, // For smooth y-axis buzzing
|
|
};
|
|
|
|
// Initial random position
|
|
flyGroup.position.set(
|
|
(Math.random() - 0.5) * (ROOM_SIZE - 4),
|
|
FLIGHT_HEIGHT_MIN + Math.random() * (FLIGHT_HEIGHT_MAX - FLIGHT_HEIGHT_MIN),
|
|
(Math.random() - 0.5) * (ROOM_SIZE - 4)
|
|
);
|
|
|
|
return flyGroup;
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates a new Quadratic Bezier curve for a fly's flight path.
|
|
* @param {THREE.Group} fly - The fly mesh group.
|
|
* @param {THREE.Vector3} endPoint - The target position for the end of the curve.
|
|
*/
|
|
function createFlyCurve(fly, endPoint) {
|
|
const startPoint = fly.position.clone();
|
|
|
|
// Calculate the midpoint
|
|
const midPoint = new THREE.Vector3().lerpVectors(startPoint, endPoint, 0.5);
|
|
|
|
// Calculate a random offset for the control point to create curvature
|
|
const offsetMagnitude = startPoint.distanceTo(endPoint) * 0.5;
|
|
const offsetAngle = Math.random() * Math.PI * 2;
|
|
|
|
// Displace the control point randomly to create a swooping path.
|
|
// Control point y is usually higher than start/end for a nice arc.
|
|
const controlPoint = new THREE.Vector3(
|
|
midPoint.x + Math.cos(offsetAngle) * offsetMagnitude * 0.5,
|
|
midPoint.y + Math.random() * 0.5 + 0.5,
|
|
midPoint.z + Math.sin(offsetAngle) * offsetMagnitude * 0.5
|
|
);
|
|
|
|
fly.userData.curve = new THREE.QuadraticBezierCurve3(
|
|
startPoint,
|
|
controlPoint,
|
|
endPoint
|
|
);
|
|
fly.userData.t = 0; // Reset progression
|
|
fly.userData.landCheckTimer = 50 + Math.random() * 50; // New landing decision window
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates and places the 'bugs' (spiders) meshes.
|
|
*/
|
|
function setupBugs() {
|
|
for (let i = 0; i < BUGS_COUNT; i++) {
|
|
const bug = createBugMesh();
|
|
bug.position.x = (Math.random() - 0.5) * (ROOM_SIZE - 2);
|
|
bug.position.z = (Math.random() - 0.5) * (ROOM_SIZE - 2);
|
|
bug.rotation.y = Math.random() * Math.PI * 2;
|
|
bug.velocity = new THREE.Vector3(
|
|
(Math.random() - 0.5) * BUG_SPEED,
|
|
0,
|
|
(Math.random() - 0.5) * BUG_SPEED
|
|
);
|
|
scene.add(bug);
|
|
bugs.push(bug);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates and places the 'flies' meshes.
|
|
*/
|
|
function setupFlies() {
|
|
for (let i = 0; i < FLIES_COUNT; i++) {
|
|
const fly = createFlyMesh();
|
|
scene.add(fly);
|
|
flies.push(fly);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates the position and state of the flies using Bezier curves.
|
|
*/
|
|
function updateFlies() {
|
|
const boundaryLimit = ROOM_SIZE / 2 - 0.1;
|
|
|
|
flies.forEach(fly => {
|
|
const data = fly.userData;
|
|
|
|
if (data.state === 'flying') {
|
|
|
|
if (!data.curve) {
|
|
// Initialize the first curve
|
|
const newTargetPos = new THREE.Vector3(
|
|
(Math.random() - 0.5) * (ROOM_SIZE - 4),
|
|
FLIGHT_HEIGHT_MIN + Math.random() * (FLIGHT_HEIGHT_MAX - FLIGHT_HEIGHT_MIN),
|
|
(Math.random() - 0.5) * (ROOM_SIZE - 4)
|
|
);
|
|
createFlyCurve(fly, newTargetPos);
|
|
}
|
|
|
|
// Advance curve progression
|
|
data.t += FLY_FLIGHT_SPEED_FACTOR;
|
|
|
|
// Check for landing readiness during the flight path
|
|
data.landCheckTimer--;
|
|
|
|
if (data.t >= 1) {
|
|
// Path finished
|
|
|
|
// 1. Check for landing decision
|
|
if (data.landCheckTimer <= 0 && Math.random() < 0.8) {
|
|
|
|
// Raycast down from the current position to find a landing spot
|
|
raycaster.set(fly.position, new THREE.Vector3(0, -1, 0));
|
|
const intersects = raycaster.intersectObjects(landingSurfaces, false);
|
|
|
|
if (intersects.length > 0) {
|
|
const intersect = intersects[0];
|
|
data.state = 'landed';
|
|
// Land slightly above the surface
|
|
fly.position.y = intersect.point.y + 0.05;
|
|
data.landTimer = 50 + Math.random() * 200; // Land for a random duration
|
|
return; // Stop updates for this fly
|
|
}
|
|
}
|
|
|
|
// 2. If not landing, generate a new random flight path
|
|
const newTargetPos = new THREE.Vector3(
|
|
(Math.random() - 0.5) * (ROOM_SIZE - 4),
|
|
FLIGHT_HEIGHT_MIN + Math.random() * (FLIGHT_HEIGHT_MAX - FLIGHT_HEIGHT_MIN),
|
|
(Math.random() - 0.5) * (ROOM_SIZE - 4)
|
|
);
|
|
createFlyCurve(fly, newTargetPos);
|
|
data.t = 0; // Reset T for the new curve
|
|
}
|
|
|
|
// Set position along the curve
|
|
fly.position.copy(data.curve.getPoint(Math.min(data.t, 1)));
|
|
|
|
// Set rotation tangent to the curve
|
|
const tangent = data.curve.getTangent(Math.min(data.t, 1)).normalize();
|
|
fly.rotation.y = Math.atan2(tangent.x, tangent.z);
|
|
|
|
// Add slight Y oscillation for buzzing feel (on top of curve)
|
|
data.oscillationTime += 0.1;
|
|
fly.position.y += Math.sin(data.oscillationTime * 4) * 0.01;
|
|
|
|
} else if (data.state === 'landed') {
|
|
// --- Landed State ---
|
|
data.landTimer--;
|
|
if (data.landTimer <= 0) {
|
|
// Take off: Generate new flight curve from current landed position
|
|
data.state = 'flying';
|
|
|
|
const newTargetPos = new THREE.Vector3(
|
|
(Math.random() - 0.5) * (ROOM_SIZE - 4),
|
|
FLIGHT_HEIGHT_MIN + Math.random() * (FLIGHT_HEIGHT_MAX - FLIGHT_HEIGHT_MIN),
|
|
(Math.random() - 0.5) * (ROOM_SIZE - 4)
|
|
);
|
|
createFlyCurve(fly, newTargetPos);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sets up the post-processing effects (Bloom).
|
|
*/
|
|
function setupPostProcessing() {
|
|
composer = new THREE.EffectComposer(renderer);
|
|
composer.addPass(new THREE.RenderPass(scene, camera));
|
|
|
|
const bloomPass = new THREE.UnrealBloomPass(
|
|
new THREE.Vector2(window.innerWidth, window.innerHeight),
|
|
bloomParams.bloomStrength,
|
|
bloomParams.bloomRadius,
|
|
bloomParams.bloomThreshold
|
|
);
|
|
composer.addPass(bloomPass);
|
|
|
|
// FXAA Pass for anti-aliasing after post-processing
|
|
const fxaaPass = new THREE.ShaderPass(THREE.FXAAShader);
|
|
fxaaPass.uniforms['resolution'].value.set(1 / window.innerWidth, 1 / window.innerHeight);
|
|
composer.addPass(fxaaPass);
|
|
}
|
|
|
|
|
|
/**
|
|
* Main animation loop.
|
|
*/
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
|
|
// Update bug positions and rotation
|
|
bugs.forEach(bug => {
|
|
// Apply current velocity
|
|
bug.position.add(bug.velocity);
|
|
|
|
// Simple collision detection with room boundaries (bounce back)
|
|
const limit = ROOM_SIZE / 2 - 0.2;
|
|
if (bug.position.x > limit || bug.position.x < -limit) {
|
|
bug.velocity.x *= -1;
|
|
// Adjust bug rotation to face the new direction
|
|
bug.rotation.y = Math.atan2(bug.velocity.x, bug.velocity.z);
|
|
}
|
|
if (bug.position.z > limit || bug.position.z < -limit) {
|
|
bug.velocity.z *= -1;
|
|
// Adjust bug rotation to face the new direction
|
|
bug.rotation.y = Math.atan2(bug.velocity.x, bug.velocity.z);
|
|
}
|
|
|
|
// Small random change in direction for erratic movement (like a real bug)
|
|
bug.velocity.x += (Math.random() - 0.5) * 0.0005;
|
|
bug.velocity.z += (Math.random() - 0.5) * 0.0005;
|
|
|
|
// Clamp velocity to prevent running too fast
|
|
bug.velocity.clampLength(0, BUG_SPEED * 1.5);
|
|
|
|
// Smooth rotation towards the direction of travel
|
|
const targetYRotation = Math.atan2(bug.velocity.x, bug.velocity.z);
|
|
bug.rotation.y += (targetYRotation - bug.rotation.y) * 0.1;
|
|
});
|
|
|
|
// Update fly positions and state
|
|
updateFlies();
|
|
|
|
// Camera Look Around (Dampened rotation)
|
|
camera.rotation.y += (targetRotation.y - camera.rotation.y) * DAMPING_FACTOR;
|
|
|
|
// Render the scene with post-processing effects
|
|
composer.render();
|
|
}
|
|
|
|
// --- Event Handlers ---
|
|
|
|
/**
|
|
* Handles window resize to maintain aspect ratio and prevent distortion.
|
|
*/
|
|
function onWindowResize() {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
composer.setSize(window.innerWidth, window.innerHeight); // Update composer size too
|
|
// Also update FXAA resolution uniform
|
|
if (composer && composer.passes && composer.passes.length > 2) { // Check if FXAA pass exists
|
|
composer.passes[2].uniforms['resolution'].value.set(1 / window.innerWidth, 1 / window.innerHeight);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles mouse movement to control the camera's rotation (look around).
|
|
* @param {MouseEvent} event
|
|
*/
|
|
function onMouseMove(event) {
|
|
// Calculate normalized device coordinates (-1 to +1)
|
|
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
pointer.y = - (event.clientY / window.innerHeight) * 2 + 1;
|
|
|
|
// Simple map of mouse position to target rotation angles
|
|
targetRotation.y = -pointer.x * degToRad(30);
|
|
}
|
|
|
|
// Wait for the window to load before initializing the 3D scene
|
|
window.onload = function() {
|
|
try {
|
|
init();
|
|
} catch (e) {
|
|
console.error("Three.js initialization failed:", e);
|
|
document.body.innerHTML = `
|
|
<div class="flex items-center justify-center h-screen bg-gray-900 text-white">
|
|
<p class="text-xl p-4 bg-red-700 rounded-lg">Error loading 3D scene. Console has more details.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
};
|
|
|
|
</script>
|
|
</body>
|
|
</html> |