使用 Three.js 和 Cannon.js 构建真实 3D 骰子动画
3D 场景搭建
Cannon.js
设置重力。
this.world.gravity.set(0, 0, -9.8 * 16);
设置骰子
、地面
以及墙体
三种材质,并两两设置联系材质。
this.diceBodyMaterial = new CANNON.Material();const deskBodyMaterial = new CANNON.Material();const barrierBodyMaterial = new CANNON.Material();this.world.addContactMaterial(new CANNON.ContactMaterial(deskBodyMaterial, this.diceBodyMaterial, {friction: 0.01,restitution: 0.5,}));this.world.addContactMaterial(new CANNON.ContactMaterial(barrierBodyMaterial, this.diceBodyMaterial, {friction: 0,restitution: 0.5,}));this.world.addContactMaterial(new CANNON.ContactMaterial(this.diceBodyMaterial, this.diceBodyMaterial, {friction: 0,restitution: 0.5,}));
添加地面以及墙体模型
this.world.addBody(new CANNON.Body({mass: 0,shape: new CANNON.Plane(),material: deskBodyMaterial,}));let barrier = new CANNON.Body({mass: 0,shape: new CANNON.Plane(),material: barrierBodyMaterial,})barrier.position.set(0, this.dimentions.height * 0.93, 0);barrier.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), Math.PI / 2);this.world.addBody(barrier);barrier = new CANNON.Body({mass: 0,shape: new CANNON.Plane(),material: barrierBodyMaterial,})barrier.position.set(0, -this.dimentions.height * 0.93, 0);barrier.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);this.world.addBody(barrier);barrier = new CANNON.Body({mass: 0,shape: new CANNON.Plane(),material: barrierBodyMaterial,})barrier.position.set(this.dimentions.width * 0.93, 0, 0);barrier.quaternion.setFromAxisAngle(new CANNON.Vec3(0, 1, 0), -Math.PI / 2);this.world.addBody(barrier);barrier = new CANNON.Body({mass: 0,shape: new CANNON.Plane(),material: barrierBodyMaterial,})barrier.position.set(-this.dimentions.width * 0.93, 0, 0);barrier.quaternion.setFromAxisAngle(new CANNON.Vec3(0, 1, 0), Math.PI / 2);this.world.addBody(barrier);
Three.js
创建场景
this.scene = new THREE.Scene();
设置环境光源
const light = new THREE.AmbientLight(0xf0f5fb, 0.8);this.scene.add(light);
添加渲染器,初始化抗锯齿与透明功能
this.renderer = new THREE.WebGLRenderer({canvas: this.canvas,antialias: true,alpha: true,});
配置渲染器
this.renderer.shadowMap.enabled = true;this.renderer.shadowMap.type = THREE.PCFShadowMap;this.renderer.setClearColor(0x000000, 0);this.renderer.setPixelRatio(devicePixelRatio);
设置渲染尺寸
this.renderer.setSize(this.canvas.clientWidth, this.canvas.clientHeight);
添加相机
const aspect = Math.min(this.canvas.clientWidth / this.dimentions.width, this.canvas.clientHeight / this.dimentions.height);const cameraZ = this.canvas.clientHeight / aspect / Math.tan(10 * Math.PI / 180);if (this.camera) this.scene.remove(this.camera);const y = this.camera ? this.camera.position.y * cameraZ / this.camera.position.z : -10;this.camera = new THREE.PerspectiveCamera(20, this.canvas.clientWidth / this.canvas.clientHeight, 1, cameraZ * 2);this.camera.position.set(0, y, cameraZ);
添加轨道控制器并限制角度
this.control = new OrbitControls(this.camera, this.canvas);this.control.enableDamping = true;this.control.dampingFactor = 0.1;this.control.enableZoom = false;this.control.enablePan = false;this.control.minPolarAngle = Math.PI * 0.5;this.control.maxPolarAngle = Math.PI * 0.9;this.control.minAzimuthAngle = 0;this.control.maxAzimuthAngle = 0;
添加点光源
const longSide = Math.max(this.dimentions.width, this.dimentions.height);if (this.light) this.scene.remove(this.light);this.light = new THREE.SpotLight(0xefdfd5, 2);this.light.position.set(-longSide / 2, longSide / 2, longSide * 2);this.light.target.position.set(0, 0, 0);this.light.distance = longSide * 5;this.light.castShadow = true;this.light.shadow.camera.near = longSide / 10;this.light.shadow.camera.far = longSide * 5;this.light.shadow.camera.fov = 50;this.light.shadow.bias = 0.001;this.light.shadow.mapSize.set(4096, 4096);this.scene.add(this.light);
添加桌面并设置阴影,导入材质
if (this.desk) this.scene.remove(this.desk);const loader = new THREE.TextureLoader();this.desk = new THREE.Mesh(new THREE.PlaneGeometry(this.dimentions.width * 2, this.dimentions.height * 2, 1, 1),new THREE.MeshPhongMaterial({map: loader.load('/assets/model/WoodFineDark.jpg'),reflectivity: 1,specular: 0x222222,shininess: 20,}),);this.desk.receiveShadow = true;this.scene.add(this.desk);
开始渲染
场景初始化完成后即可开始渲染,这里通过requestAnimationFrame
递归调用函数进行动画渲染
function render() {component.tick();requestAnimationFrame(render);component.renderer.render(component.scene, component.camera);}