今天郭先生繼續說cannon.js,主演內容就是點對點約束和2D座標轉3D座標。仍然以一個案例為例,場景由一個地面、若干網格組成的約束體和一些擁有初速度的球體組成,如下圖。線案例請點選部落格原文。
下面來說說如何使用約束來完成一個這樣的物理場景。
1. 建立three場景
這一步是基礎工作,對於有一定three基礎的同學都不會陌生,我就直接上程式碼了。
initThree() { scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 1000 ); camera.position.x = 40; camera.position.y = 52; camera.position.z = 78; scene.add( camera ); scene.add(new THREE.AxesHelper(40)); scene.add(new THREE.AmbientLight(0x888888)); const light = new THREE.DirectionalLight(0xbbbbbb, 1); light.position.set(0, 50, 50); const distance = 200; let texture = new THREE.TextureLoader().load('/static/images/base/ground.png'); texture.wrapS = texture.wrapT = THREE.RepeatWrapping; texture.repeat.copy(new THREE.Vector2(40, 40)); let groundGeom = new THREE.BoxBufferGeometry(100, 0.2, 100); let groundMate = new THREE.MeshPhongMaterial({color: 0xdddddd, map: texture}) ground = new THREE.Mesh(groundGeom, groundMate); ground.position.y = -0.1; ground.receiveShadow = true; scene.add(ground); geometry = new THREE.BoxGeometry( 2, 2, 2 ); renderer = new THREE.WebGLRenderer({antialias: true}); renderer.setSize( window.innerWidth, window.innerHeight ); renderer.shadowMap.enabled = true; renderer.setClearColor(0xbfd1e5); controls = new OrbitControls(camera, renderer.domElement); controls.target.set(0, 10, 0); camera.lookAt(0,10,0); this.$refs.box.appendChild( renderer.domElement ); stats = new Stats(); this.$refs.box.appendChild(stats.dom); },
這裡面主要進行初始化場景、相機、渲染器、燈光和地面等操作。
2. 初始化物理世界
這裡麵包括建立CANNON.World,建立地面剛體,每塊需要被約束的剛體和設定點對點約束(在給定的偏移點連線兩個實體),接下來我們仍以程式碼註釋的形式詳細的講解對於物理世界的建立。
initCannon() { world = new CANNON.World(); world.gravity.set(0, -9.8, 0); world.broadphase = new CANNON.NaiveBroadphase(); world.solver.iterations = 10; bodyGround = new CANNON.Body({ mass: 0, position: new CANNON.Vec3(0, -0.1, 0), shape: new CANNON.Box(new CANNON.Vec3(50, 0.1, 50)), material: new CANNON.Material({friction: 0.05, restitution: params.restitution}) }); ground.userData = bodyGround; world.addBody(bodyGround); //上面的程式碼意義上一節已經講過了,我就不多言,主要看下面的程式碼。 //這裡設定了一些變數,N表示組成約束體剛體的數量,space表示相鄰兩個剛體直接的距離間隔,mass為剛體的質量變數,width表示剛體半寬度,height表示剛體半高度,last表示上一個相連的剛體。 var N = 20, space = 0.1, mass = 0, width = 10, hHeight = 1, last; var halfVec = new CANNON.Vec3(width, hHeight, 0.2);//剛體的長寬高的halfSize向量 var boxShape = new CANNON.Box(halfVec);//定義一個長方體資料 var boxGeometry = new THREE.BoxBufferGeometry(halfVec.x * 2, halfVec.y * 2, halfVec.z * 2);//定義一個長方几何體 var boxMaterial = new THREE.MeshLambertMaterial( { color: 0xffaa00 } );//定義幾何體材質 for(var i=0; i<N; i++) {//遍歷N次,從上到下建立長方體網格和剛體,位置逐漸變低,質量逐漸變小。 var boxBody = new CANNON.Body({mass: mass, material: new CANNON.Material({friction: 0.05, restitution: params.restitution})});//建立剛體,第一個剛體的質量設定成0(即為不動的剛體),定義材質,並設定摩擦係數和彈性係數 boxBody.addShape(boxShape);//為剛體新增形狀 var boxMesh = new THREE.Mesh(boxGeometry, boxMaterial);//建立three世界的網格 boxBody.position.set(0, (N - i + 5) * (hHeight * 2 + space * 2), 0);//這裡設定剛體的位置,是由上倒下的順序 boxBody.linearDamping = 0.01;//設定線性阻尼 boxBody.angularDamping = 0.01;//設定旋轉阻尼 world.addBody(boxBody);//將剛體新增到物理世界中 scene.add(boxMesh);//將網格新增到three場景中 boxes.push(boxBody);//將剛體新增到陣列中 boxMeshes.push(boxMesh);//將網格新增到陣列中,這兩步可以在更新物理世界中找到他們的對應關係,也可以新增到Mesh的userData屬性中去,具體可以參見上一篇文章 if(i == 0) { //當i=0時,也就是第一個剛體,在剛體建立完畢後,我們將mass變數設定成1 mass = 1; } else {//從第二個剛體往後都會建立兩個點對點的約束,點對點約束我們下面講 var ptp1 = new CANNON.PointToPointConstraint(boxBody, new CANNON.Vec3(-width, hHeight + space, 0), last, new CANNON.Vec3(-width, -hHeight - space, 0), (N - i) / 4); var ptp2 = new CANNON.PointToPointConstraint(boxBody, new CANNON.Vec3(width, hHeight + space, 0), last, new CANNON.Vec3(width, -hHeight - space, 0), (N - i) / 4); world.addConstraint(ptp1);//將約束新增到物理世界 world.addConstraint(ptp2);//將約束新增到物理世界 } last = boxBody;//這裡將本次建立的剛體賦值給last變數,一遍下一個迴圈使用 } },
我們來說說這個點對點約束,他時由5個引數組成
PointToPointConstraint ( bodyA pivotA bodyB pivotB maxForce )
- bodyA – 剛體A
- pivotA – 相對於剛體A質心的點,剛體A被約束到該點。
- bodyB – 將被約束到與剛體A相同的點的主體。因此,我們將獲得剛體A和剛體B之間的連結。如果未指定,剛體A將被約束到一個靜態點。
- pivotB – 相對於剛體B質心的點,剛體B被約束到該點。
- maxForce – 約束物體應施加的最大力(如果施加的力過大,剛體A和剛體B之間的連結就會被拉長)
下面就是我們設定連結點的示意圖,這樣我們就可以清楚上面的程式碼了
3. 根據滑鼠點選,發射一個剛體球
這裡就要應用到2D座標轉3D座標的一些知識了,這裡網上已經有很多相關的知識了,可以看threejs 世界座標與螢幕座標相互轉換,這裡我就直接上程式碼了
document.addEventListener('click', event => { //點選滑鼠 event.preventDefault();//阻止預設事件 let x = (event.clientX / window.innerWidth) * 2 - 1;//將滑鼠點選的x值轉換成[-1, 1] let y = - (event.clientY / window.innerHeight) * 2 + 1;//將滑鼠點選的y值轉換成[-1, 1] let p = new THREE.Vector3(x, y, -1).unproject(camera);//通過unproject方法,使用所傳入的攝像機來反投影(projects)該向量,得到滑鼠對應三維空間點 let v = p.sub(camera.position).normalize();//用滑鼠對應的三維空間點減去相機的位置向量,然後歸一化得到小球的射出方向的單位向量 this.createSphere(v, camera.position);//把需要的兩個向量傳入建立小球的方法中 }) createSphere(v, c) { //建立小球的方法和上一篇很相似,我就不贅述了 const speed = 50; var geometry = new THREE.SphereBufferGeometry(1.5, 32, 16); let sphere = new THREE.Mesh( geometry, this.createRandomMaterial()); sphere.position.copy(c); sphere.castShadow = true; sphere.receiveShadow = true; scene.add( sphere ); ballMeshes.push(sphere); let sphereBody = new CANNON.Body({ mass: params.mass, position: new CANNON.Vec3(c.x, c.y, c.z), shape: new CANNON.Sphere(1.5), material: new CANNON.Material({friction: 0.1, restitution: params.restitution}) }); sphereBody.collisionResponse = 0.01; sphereBody.velocity.set(v.x * speed, v.y * speed, v.z * speed);//這裡要注意velocity屬性可以剛體帶有出速度 world.addBody(sphereBody); balls.push(sphereBody) setTimeout(() => { scene.remove(sphere); sphere.material.dispose(); sphere.geometry.dispose(); world.removeBody(sphereBody); balls.shift(); ballMeshes.shift(); }, 60000) } createRandomMaterial() { color.setHSL(Math.random(), 1.0, 0.5); return new THREE.MeshPhongMaterial({color: color}); }
這樣就完成了點對點約束的物理效果,讓原本虛擬的three世界變得更加真實。
轉載請註明地址:郭先生的部落格