import * as THREE from 'three'; import Stats from 'three/addons/libs/stats.module.js'; import { RapierPhysics } from 'three/addons/physics/RapierPhysics.js'; 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'; 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); } 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); camera.position.set(-5, 3, -5); camera.lookAt(0, 0.5, 0); camera.far = 100; 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(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(); 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); }; 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(); composer.render(renderer, scene, camera); } renderer.setAnimationLoop(loop); })();