diff --git a/main.js b/main.js index 0e271e8..088bcde 100644 --- a/main.js +++ b/main.js @@ -5,58 +5,470 @@ import { RapierHelper } from 'three/addons/helpers/RapierHelper.js'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js'; import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { SSAOPass } from 'three/addons/postprocessing/SSAOPass.js'; +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; -var renderer, camera, scene, car, stats, container; +class ThirdPersionControls extends THREE.Controls { + constructor(object, domElement) { + super(object, domElement); + this.offset = new THREE.Vector3(0, 1, -5); + this.targetPosition = new THREE.Vector3(0, 0, 0); + this.horizontalSpringConstant = 0.5; + this.horizontalDampingConstant = 0.3; + this.velocity = new THREE.Vector3(0, 0, 0); + } -(() => { - container = document.getElementById('container'); + update( deltaTime = null ) { + + + } +} + +let renderer, camera, scene, stats, container, sun, controls, composer, clock = new THREE.Clock(); +let car, chassis, wheels, headLightLeft, headLightRight, brakeLightLeft, brakeLightRight; +let physics, physicsHelper, vehicleController, movement; +let focus = true; + +function init() { stats = new Stats(); renderer = new THREE.WebGLRenderer({ antialias: true }); camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 100); scene = new THREE.Scene(); + container.appendChild(stats.dom); container.appendChild(renderer.domElement); // setup renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); + renderer.shadowMap.enabled = true; - scene.background = new THREE.Color( 0xbfe3dd ); + scene.background = new THREE.Color(0xbfe3dd); - camera.position.set(0, 1, 5); + camera.position.set(-5, 3, -5); + camera.lookAt(0, 0.5, 0); + camera.far = 100; - var sun = new THREE.DirectionalLight(0xffffff, 1.5); - sun.position.set(10, 10, 10); + controls = new ThirdPersionControls(camera, renderer.domElement); + controls.update(); + + composer = new EffectComposer( renderer ); + + const renderPass = new RenderPass( scene, camera ); + composer.addPass( renderPass ); + + const width = window.innerWidth; + const height = window.innerHeight; + const ssaoPass = new SSAOPass( scene, camera, width, height ); + ssaoPass.output = SSAOPass.OUTPUT.Default; + ssaoPass.kernelRadius = 64; + composer.addPass( ssaoPass ); + + const outputPass = new OutputPass(); + composer.addPass( outputPass ); +} + +function setupLights() { + sun = new THREE.DirectionalLight(0xffffff, 1.5); + sun.position.set(10, 10, 3); + sun.castShadow = true; + sun.shadow.mapSize.width = sun.shadow.mapSize.height = 1024 * 2; + sun.shadow.camera.near = 10; + sun.shadow.camera.far = 50; + sun.shadow.camera.left = -2; + sun.shadow.camera.right = 2; + sun.shadow.camera.top = 2; + sun.shadow.camera.bottom = -2; + sun.shadow.bias = -0.001; + sun.shadow.radius = 2; scene.add(sun); - var ambientLight = new THREE.AmbientLight(0x404040); + var ambientLight = new THREE.AmbientLight(0xffffff, 0.3); scene.add(ambientLight); + //const cameraHelper = new THREE.CameraHelper(sun.shadow.camera); + //scene.add(cameraHelper); +} + +function setupWorld() { + var groundPlane = new THREE.Mesh( + new THREE.BoxGeometry(100, 0.5, 100), + new THREE.MeshStandardMaterial({ color: 0x808080 }) + ); + + groundPlane.receiveShadow = true; + groundPlane.castShadow = false; + groundPlane.userData = { physics: { mass: 0 } }; // static object + groundPlane.position.set(0, -0.25, 0); + scene.add(groundPlane); +} + +function addWheel(index, pos, carMesh, model) { + const wheelRadius = 0.2; + const wheelWidth = 0.16; + const suspensionRestLength = 0.04; + const wheelPosition = pos; // Position relative to chassis + const wheelDirection = { x: 0.0, y: - 1.0, z: 0.0 }; // Downward direction + const wheelAxle = { x: 1, y: 0.0, z: 0.0 }; // Axle direction + + // Add the wheel to the vehicle controller + vehicleController.addWheel( + wheelPosition, + wheelDirection, + wheelAxle, + suspensionRestLength, + wheelRadius + ); + + // Set suspension stiffness for wheel + vehicleController.setWheelSuspensionStiffness(index, 45.0); + vehicleController.setWheelSuspensionCompression(index, 0.8); + vehicleController.setWheelSuspensionRelaxation(index, 0.6); + + // Set wheel friction + vehicleController.setWheelFrictionSlip(index, 100.0); + vehicleController.setWheelSideFrictionStiffness(index, 2.0); + + // Enable steering for the wheel + vehicleController.setWheelSteering(index, pos.z > 0); + + // Create a wheel mesh + const geometry = new THREE.CylinderGeometry(wheelRadius, wheelRadius, wheelWidth, 16); + //geometry.rotateZ(Math.PI * 0.5); + const material = new THREE.MeshStandardMaterial({ visible: false }); + const wheel = new THREE.Mesh(geometry, material); + + var m = model.clone(); + if (pos.x < 0) { + m.rotateZ(Math.PI); + } + wheel.add(m); + + wheel.castShadow = false; + + wheel.position.copy(pos); + + wheels.push(wheel); + carMesh.add(wheel); +} + +function createCar() { const loader = new GLTFLoader(); - loader.load('Miata.glb', (gltf) => { - car = gltf.scene; - car.position.set(0, 0, 0); - car.scale.set(1, 1, 1); - scene.add(car); + + const geometry = new THREE.BoxGeometry(1, 0.45, 2.5); + const material = new THREE.MeshStandardMaterial({ visible: false }) + const mesh = new THREE.Mesh(geometry, material); + + // + scene.add(mesh); + car = mesh; + + mesh.position.y = 1; + + physics.addMesh(mesh, 1100, 0.2); // addMesh places the RigidBody in the mesh.userData.physics object + chassis = mesh.userData.physics.body; + //chassis.linearDamping = 0.2; + //chassis.angularDamping = 0.3; + + vehicleController = physics.world.createVehicleController(chassis); + vehicleController.setIndexForwardAxis = 2; + vehicleController.indexUpAxis = 0; + console.log(); + + wheels = []; + + loader.load('Miata_wheel.glb', (gltf) => { + gltf.scene.children[0].traverse((child) => { + if (child.isMesh) { + child.castShadow = true; + child.receiveShadow = true; + } + }); + gltf.scene.children[0].position.set(0, 0, 0); + const model = gltf.scene.children[0]; + addWheel(0, { x: - 0.49, y: -0.2, z: - 0.75 }, mesh, model); + addWheel(1, { x: 0.49, y: -0.2, z: - 0.75 }, mesh, model); + addWheel(2, { x: - 0.49, y: -0.2, z: 0.8 }, mesh, model); + addWheel(3, { x: 0.49, y: -0.2, z: 0.8 }, mesh, model); + //vehicleController.setWheelSteering( 2, Math.PI / 4 ); + //vehicleController.setWheelSteering( 3, Math.PI / 4 ); }); + loader.load('Miata.glb', (gltf) => { + const m = gltf.scene; + m.traverse((child) => { + if (child.isMesh) { + child.castShadow = true; + child.receiveShadow = true; + } + }); + m.position.set(0, -0.35, 0); + m.scale.set(1.0, 1.0, 1.0); + + mesh.add(m); + }); + + headLightLeft = new THREE.SpotLight(0xffffff, 3.0, 20, Math.PI / 4, 0.2); + headLightLeft.position.set(-0.35, 0.3, 1.1); + headLightLeft.castShadow = true; + headLightLeft.shadow.mapSize.width = 1024; + headLightLeft.shadow.mapSize.height = 1024; + headLightLeft.shadow.camera.near = 0.5; + headLightLeft.shadow.camera.far = 10; + headLightLeft.shadow.camera.fov = 30; + headLightLeft.shadow.bias = -0.001; + headLightLeft.shadow.radius = 3; + + var headLightLeftTarget = new THREE.Object3D(); + headLightLeftTarget.position.set(-0.35, 0.25, 2); + headLightLeft.target = headLightLeftTarget; + mesh.add(headLightLeftTarget); + mesh.add(headLightLeft); + + headLightRight = new THREE.SpotLight(0xffffff, 3.0, 20, Math.PI / 4, 0.2); + headLightRight.position.set(0.35, 0.3, 1.1); + headLightRight.castShadow = true; + headLightRight.shadow.mapSize.width = 1024; + headLightRight.shadow.mapSize.height = 1024; + headLightRight.shadow.camera.near = 0.5; + headLightRight.shadow.camera.far = 10; + headLightRight.shadow.camera.fov = 30; + headLightRight.shadow.bias = -0.001; + headLightRight.shadow.radius = 3; + + var headLightRightTarget = new THREE.Object3D(); + headLightRightTarget.position.set(0.35, 0.25, 2); + headLightRight.target = headLightRightTarget; + mesh.add(headLightRightTarget); + mesh.add(headLightRight); + + brakeLightLeft = new THREE.SpotLight(0xff0000, 2.5, 5, Math.PI / 4, 0.2); + brakeLightLeft.position.set(-0.4, 0.2, -1.3); + brakeLightLeft.castShadow = true; + brakeLightLeft.shadow.mapSize.width = 1024; + brakeLightLeft.shadow.mapSize.height = 1024; + brakeLightLeft.shadow.camera.near = 0.5; + brakeLightLeft.shadow.camera.far = 10; + brakeLightLeft.shadow.camera.fov = 30; + brakeLightLeft.shadow.bias = -0.001; + brakeLightLeft.shadow.radius = 5; + + var brakeLightLeftTarget = new THREE.Object3D(); + brakeLightLeftTarget.position.set(-0.4, 0.2, -3); + brakeLightLeft.target = brakeLightLeftTarget; + mesh.add(brakeLightLeftTarget); + mesh.add(brakeLightLeft); + + brakeLightRight = new THREE.SpotLight(0xff0000, 2.5, 5, Math.PI / 4, 0.2); + brakeLightRight.position.set(0.4, 0.2, -1.3); + brakeLightRight.castShadow = true; + brakeLightRight.shadow.mapSize.width = 1024; + brakeLightRight.shadow.mapSize.height = 1024; + brakeLightRight.shadow.camera.near = 0.5; + brakeLightRight.shadow.camera.far = 10; + brakeLightRight.shadow.camera.fov = 30; + brakeLightRight.shadow.bias = -0.001; + brakeLightRight.shadow.radius = 5; + + var brakeLightRightTarget = new THREE.Object3D(); + brakeLightRightTarget.position.set(0.4, 0.2, -3); + brakeLightRight.target = brakeLightRightTarget; + mesh.add(brakeLightRightTarget); + mesh.add(brakeLightRight); + +} + +async function initPhysics() { + physics = await RapierPhysics(); + physicsHelper = new RapierHelper(physics.world); + scene.add(physicsHelper); + physics.addScene(scene); +} + +function updateWheels() { + if (vehicleController === undefined) return; + + const wheelSteeringQuat = new THREE.Quaternion(); + const wheelRotationQuat = new THREE.Quaternion(); + const up = new THREE.Vector3(0, 1, 0); + + //const chassisPosition = chassis.translation(); + + wheels.forEach((wheel, index) => { + + const wheelAxleCs = vehicleController.wheelAxleCs(index); + const connection = vehicleController.wheelChassisConnectionPointCs(index).y || 0; + const suspension = vehicleController.wheelSuspensionLength(index) || 0; + const steering = vehicleController.wheelSteering(index) || 0; + const rotationRad = vehicleController.wheelRotation(index) || 0; + + wheel.position.y = connection - suspension; + + wheelSteeringQuat.setFromAxisAngle(up, steering); + wheelRotationQuat.setFromAxisAngle(wheelAxleCs, rotationRad); + + wheel.quaternion.set(0, 0, 0, 1); + wheel.quaternion.multiply(wheelSteeringQuat); + wheel.quaternion.multiply(wheelRotationQuat); + + }); +} + +function updateCarControl() { + let accelerateForce = 0; + if (chassis.isSleeping()) chassis.wakeUp(); + if (movement.forward < 0) { + + //if (movement.accelerateForce.value === 0) chassis.wakeUp(); + //accelerateForce = movement.accelerateForce.value - movement.accelerateForce.step; + //if (accelerateForce < movement.accelerateForce.min) accelerateForce = movement.accelerateForce.min; + accelerateForce = movement.accelerateForce.max; + + } else if (movement.forward > 0) { + + //if (movement.accelerateForce.value === 0) chassis.wakeUp(); + //accelerateForce = movement.accelerateForce.value + movement.accelerateForce.step; + + //if (accelerateForce > movement.accelerateForce.max) accelerateForce = movement.accelerateForce.max; + accelerateForce = movement.accelerateForce.min; + + } + + movement.accelerateForce.value = accelerateForce; + + //console.log(accelerateForce); + + let brakeForce = 0; + + if (movement.brake > 0) { + brakeLightLeft.intensity = 2.5; + brakeLightRight.intensity = 2.5; + brakeForce = movement.brakeForce.value + movement.brakeForce.step; + if (brakeForce > movement.brakeForce.max) brakeForce = movement.brakeForce.max; + + } + else { + brakeLightLeft.intensity = 0; + brakeLightRight.intensity = 0; + } + + movement.brakeForce.value = brakeForce; + + const engineForce = accelerateForce; + + vehicleController.setWheelEngineForce(0, -engineForce); + vehicleController.setWheelEngineForce(1, -engineForce); + + const currentSteering = vehicleController.wheelSteering(2); + const steerDirection = movement.right; + const steerAngle = Math.PI / 4; + + const steering = THREE.MathUtils.lerp(currentSteering, steerAngle * steerDirection, 0.02); + + vehicleController.setWheelSteering(2, steering); + vehicleController.setWheelSteering(3, steering); + + vehicleController + + const wheelBrake = movement.brake * brakeForce; + vehicleController.setWheelBrake(0, wheelBrake); + vehicleController.setWheelBrake(1, wheelBrake); + vehicleController.setWheelBrake(2, wheelBrake); + vehicleController.setWheelBrake(3, wheelBrake); +} + +(async () => { + container = document.getElementById('container'); + + movement = { + forward: 0, + right: 0, + brake: 0, + accelerateForce: { value: 0, min: - 300, max: 600, step: 50 }, + brakeForce: { value: 0, min: 0, max: 20, step: 1 } + }; + + window.addEventListener('keydown', (event) => { + if (event.key === 'w' || event.key === 'ArrowUp') movement.forward = - 1; + if (event.key === 's' || event.key === 'ArrowDown') movement.forward = 1; + if (event.key === 'a' || event.key === 'ArrowLeft') movement.right = 1; + if (event.key === 'd' || event.key === 'ArrowRight') movement.right = - 1; + if (event.key === ' ') movement.brake = 1; + }); + + window.addEventListener('keyup', (event) => { + if ((event.key === 'w' || event.key === 'ArrowUp' ) && movement.forward == -1) movement.forward = 0; + if ((event.key === 's' || event.key === 'ArrowDown') && movement.forward == 1) movement.forward = 0; + if ((event.key === 'a' || event.key === 'ArrowLeft') && movement.right == 1) movement.right = 0; + if ((event.key === 'd' || event.key === 'ArrowRight') && movement.right == -1) movement.right = 0; + if (event.key === ' ') movement.brake = 0; + }); + window.onresize = function () { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); + composer.setSize(window.innerWidth, window.innerHeight); }; - function loop() { - // rotate the car - if (car) { - car.rotation.y += 0.01; + window.addEventListener('blur', () => { + focus = false; + }); + + window.addEventListener('focus', () => { + focus = true; + }); + + + + init(); + + setupWorld(); + + setupLights(); + + await initPhysics(); + + createCar(); + + + + + function loop() { + if (document.hidden) { + focus = false; } + // if window is not focused // not visible, pause the loop + if (!focus) { + //return; + } + + let delta = clock.getDelta(); + delta = Math.min(delta, 1 / 60); // cap delta to avoid large jumps + // rotate the car + controls.targetPosition.copy(car.position); + controls.update(delta); + + // set spotlight target to forward of the car + + sun.target = car; + sun.position.copy(car.position).add(new THREE.Vector3(10, 10, 3)); + + updateCarControl(); + vehicleController.updateVehicle(delta); + updateWheels(); + if (physicsHelper) physicsHelper.update(); + //renderer.render(scene, camera); stats.update(); - renderer.render(scene, camera); + composer.render(renderer, scene, camera); + } renderer.setAnimationLoop(loop);