1. 物理エンジンと大砲の設置を理解する
Cannon.js は、WebGL で 3D 物理シミュレーションを作成するためのオープンソース 3D 物理エンジンです。多くの WebGL シナリオに適用できる柔軟な API を提供します。
物理的エンティティ、衝突、物理的変換、物理世界と物理的エンティティ間の制約など、いくつかの基本的な概念を理解する必要があります。
公式ウェブサイト: https: //pmndrs.github.io/cannon-es/
npm: https://www.npmjs.com/package/cannon-es
npm i --save cannon-es
cannon-es は、軽量で使いやすいネットワーク 3D 物理エンジンです。これは、three.js のシンプルな API からインスピレーションを受けており、ammo.js と Bullet 物理エンジンに基づいています。
最初にセットアップするのは物理世界です。これにはすべての物理エンティティが収容され、シミュレーションが進められます。
地球の重力を利用した世界を作ってみましょう。cannon.js では SI 単位 (メートル、キログラム、秒など) が使用されることに注意してください。
const world = new CANNON.World({
gravity: new CANNON.Vec3(0, -9.82, 0), // m/s²
})
シミュレーションを進めるには、フレームごとに world.fixedStep() を呼び出す必要があります。最初の引数として、シミュレーションを実行する固定タイム ステップを渡すことができます。デフォルト値は 1 / 60、つまり 60fps です。world.fixedStep() は、 requestAnimationFrame の呼び出しがデバイス間で異なる場合があるため、またはパフォーマンスの問題がある場合に、フレーム レートに関係なく同じ速度を維持するために呼び出された最後のシミュレーションを追跡します。固定アナログステッピングの詳細については、こちらをご覧ください。
function animate() {
requestAnimationFrame(animate)
// Run the simulation independently of framerate every 1 / 60 ms
world.fixedStep()
}
// Start the simulation loop
animate()
最後の呼び出し (ゲーム ワールドの dt) からの時間を経過させたい場合は、より高度な world.step() を使用できます。
高度なワールド ステッピングの例を参照してください。
const timeStep = 1 / 60 // seconds
let lastCallTime
function animate() {
requestAnimationFrame(animate)
const time = performance.now() / 1000 // seconds
if (!lastCallTime) {
world.step(timeStep)
} else {
const dt = time - lastCallTime
world.step(timeStep, dt)
}
lastCallTime = time
}
// Start the simulation loop
animate()
リジッドボディはワールド内でシミュレートされるエンティティであり、球、ボックス、平面、円柱などの単純な形状、または凸多面体、パーティクル、ハイトフィールド、トリメッシュなどのより複雑な形状にすることができます。
基本的な球体を作成しましょう。
const radius = 1 // m
const sphereBody = new CANNON.Body({
mass: 5, // kg
shape: new CANNON.Sphere(radius),
})
sphereBody.position.set(0, 10, 0) // m
world.addBody(sphereBody)
ご覧のとおり、質量アトリビュートを指定しました。質量は、力が作用したときにボディがどのように動作するかを定義します。
物体が質量を持ち、力の影響を受ける場合、それらは動的物体と呼ばれます。力の影響を受けないが速度を持って動き回る運動学的エンティティもあります。3 番目のタイプのオブジェクトは静的オブジェクトで、ワールド内にのみ配置でき、力や速度の影響を受けません。
オブジェクトに質量 0 を渡すと、そのオブジェクトは自動的に静的オブジェクトとしてマークされます。body オプションで body タイプを指定することもできます。たとえば、静的なアースを作成してみましょう。
const groundBody = new CANNON.Body({
type: CANNON.Body.STATIC,
shape: new CANNON.Plane(),
})
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0) // make it face up
world.addBody(groundBody)
以下は、以前のすべてのスニペットを完全な例に結合したものです。
import * as CANNON from 'cannon-es'
// Setup our physics world
const world = new CANNON.World({
gravity: new CANNON.Vec3(0, -9.82, 0), // m/s²
})
// Create a sphere body
const radius = 1 // m
const sphereBody = new CANNON.Body({
mass: 5, // kg
shape: new CANNON.Sphere(radius),
})
sphereBody.position.set(0, 10, 0) // m
world.addBody(sphereBody)
// Create a static plane for the ground
const groundBody = new CANNON.Body({
type: CANNON.Body.STATIC, // can also be achieved by setting the mass to 0
shape: new CANNON.Plane(),
})
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0) // make it face up
world.addBody(groundBody)
// Start the simulation loop
function animate() {
requestAnimationFrame(animate)
world.fixedStep()
// the sphere y position shows the sphere falling
console.log(`Sphere y position: ${
sphereBody.position.y}`)
}
animate()
cannon は画面に何かをレンダリングする責任はなく、シミュレーションの計算を計算するだけであることに注意してください。実際に画面に何かを表示するには、three.js などのレンダリング ライブラリを使用する必要があります。これを実現する方法を見てみましょう。
まず、three.js で body に対応するエンティティを作成する必要があります。たとえば、three.js で球を作成する方法は次のとおりです。
const radius = 1 // m
const geometry = new THREE.SphereGeometry(radius)
const material = new THREE.MeshNormalMaterial()
const sphereMesh = new THREE.Mesh(geometry, material)
scene.add(sphereMesh)
次に、three.js グリッドを cannon.js ボディに接続する必要があります。これを行うには、世界を歩き回るときにフレームごとに位置と回転のデータをボディからメッシュにコピーする必要があります。
function animate() {
requestAnimationFrame(animate)
// world stepping...
sphereMesh.position.copy(sphereBody.position)
sphereMesh.quaternion.copy(sphereBody.quaternion)
// three.js render...
}
animate()
2. 物理エンジンを使用して Threejs オブジェクトを関連付ける
物理エンジンとレンダリング エンジン レンダリング エンジンは、レンダリングのために物理エンジンからデータを取得します。
const world = new CANNON.World(); // 创建物理世界
world.gravity.set(0, -9.8, 0); // 设置重力方向
// 创建物理小球形状
const sphereShape = new CANNON.Sphere(1);
//设置物体材质
const sphereWorldMaterial = new CANNON.Material();
// 创建物理世界的物体
const sphereBody = new CANNON.Body({
shape: sphereShape,
position: new CANNON.Vec3(0, 0, 0),
// 小球质量
mass: 1,
// 物体材质
material: sphereWorldMaterial,
});
// 将物体添加至物理世界
world.addBody(sphereBody);
物理エンジンとレンダリング エンジンの関係
// 更新物理引擎里世界的物体
world.step(1 / 120, deltaTime); // 更新
sphere.position.copy(sphereBody.position); // 渲染引擎复制物理引擎中的数据 做渲染 自由落体
フリーフォール
import * as THREE from "three";
// 导入轨道控制器
import {
OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 导入动画库
import gsap from "gsap";
// 导入dat.gui
import * as dat from "dat.gui";
// 导入connon引擎
import * as CANNON from "cannon-es";
// 目标:使用cannon引擎
console.log(CANNON);
// const gui = new dat.GUI();
// 1、创建场景
const scene = new THREE.Scene();
// 2、创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
300
);
// 设置相机位置
camera.position.set(0, 0, 18);
scene.add(camera);
// 创建球和平面
const sphereGeometry = new THREE.SphereGeometry(1, 20, 20);
const sphereMaterial = new THREE.MeshStandardMaterial();
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.castShadow = true; // 阴影
scene.add(sphere);
const floor = new THREE.Mesh(
new THREE.PlaneBufferGeometry(20, 20),
new THREE.MeshStandardMaterial()
);
floor.position.set(0, -5, 0);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;// 接收阴影
scene.add(floor);
// 创建物理世界
// const world = new CANNON.World({ gravity: 9.8 });
const world = new CANNON.World();
world.gravity.set(0, -9.8, 0);
// 创建物理小球形状
const sphereShape = new CANNON.Sphere(1);
//设置物体材质
const sphereWorldMaterial = new CANNON.Material();
// 创建物理世界的物体
const sphereBody = new CANNON.Body({
shape: sphereShape,
position: new CANNON.Vec3(0, 0, 0),
// 小球质量
mass: 1,
// 物体材质
material: sphereWorldMaterial,
});
// 将物体添加至物理世界
world.addBody(sphereBody);
//添加环境光和平行光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.castShadow = true; // 阴影
scene.add(dirLight);
// 初始化渲染器
// 渲染器透明
const renderer = new THREE.WebGLRenderer({
alpha: true });
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;
// console.log(renderer);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);
// // 使用渲染器,通过相机将场景渲染进来
// renderer.render(scene, camera);
// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
controls.enableDamping = true;
// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 设置时钟
const clock = new THREE.Clock();
function render() {
// let time = clock.getElapsedTime();
let deltaTime = clock.getDelta();
// 更新物理引擎里世界的物体
world.step(1 / 120, deltaTime); // 更新
sphere.position.copy(sphereBody.position); // 渲染引擎复制物理引擎中的数据
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}
render();
// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
// console.log("画面变化了");
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比
renderer.setPixelRatio(window.devicePixelRatio);
});
3. ボールを衝突させる固定地面を設定する
ボールが地面に落ちた後、どのようにして止まるのでしょうか?
次に、物理的な世界のグラウンドも作成します
// 物理世界创建地面
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
// 当质量为0的时候,可以使得物体保持不动
floorBody.mass = 0;
floorBody.addShape(floorShape);
// 地面位置
floorBody.position.set(0, -5, 0);
// 旋转地面的位置
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(floorBody);
こうすることで、ボールが地面に当たったときに止まります。
import * as THREE from "three";
// 导入轨道控制器
import {
OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 导入动画库
import gsap from "gsap";
// 导入dat.gui
import * as dat from "dat.gui";
// 导入connon引擎
import * as CANNON from "cannon-es";
// 目标:使用cannon引擎
console.log(CANNON);
// const gui = new dat.GUI();
// 1、创建场景
const scene = new THREE.Scene();
// 2、创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
300
);
// 设置相机位置
camera.position.set(0, 0, 18);
scene.add(camera);
// 创建球和平面
const sphereGeometry = new THREE.SphereGeometry(1, 20, 20);
const sphereMaterial = new THREE.MeshStandardMaterial();
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.castShadow = true;
scene.add(sphere);
const floor = new THREE.Mesh(
new THREE.PlaneBufferGeometry(20, 20),
new THREE.MeshStandardMaterial()
);
floor.position.set(0, -5, 0);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
// 创建物理世界
// const world = new CANNON.World({ gravity: 9.8 });
const world = new CANNON.World();
world.gravity.set(0, -9.8, 0);
// 创建物理小球形状
const sphereShape = new CANNON.Sphere(1);
//设置物体材质
const sphereWorldMaterial = new CANNON.Material();
// 创建物理世界的物体
const sphereBody = new CANNON.Body({
shape: sphereShape,
position: new CANNON.Vec3(0, 0, 0),
// 小球质量
mass: 1,
// 物体材质
material: sphereWorldMaterial,
});
// 将物体添加至物理世界
world.addBody(sphereBody);
// 物理世界创建地面
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
// 当质量为0的时候,可以使得物体保持不动
floorBody.mass = 0;
floorBody.addShape(floorShape);
// 地面位置
floorBody.position.set(0, -5, 0);
// 旋转地面的位置
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(floorBody);
//添加环境光和平行光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.castShadow = true;
scene.add(dirLight);
// 初始化渲染器
// 渲染器透明
const renderer = new THREE.WebGLRenderer({
alpha: true });
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;
// console.log(renderer);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);
// // 使用渲染器,通过相机将场景渲染进来
// renderer.render(scene, camera);
// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
controls.enableDamping = true;
// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 设置时钟
const clock = new THREE.Clock();
function render() {
// let time = clock.getElapsedTime();
let deltaTime = clock.getDelta();
// 更新物理引擎里世界的物体
world.step(1 / 120, deltaTime);
sphere.position.copy(sphereBody.position);
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}
render();
// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
// console.log("画面变化了");
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比
renderer.setPixelRatio(window.devicePixelRatio);
});
3. 衝突イベントを監視し、衝突音の効果を制御する
// 创建击打声音
const hitSound = new Audio("assets/metalHit.mp3");
// 添加监听碰撞事件
function HitEvent(e) {
// 获取碰撞的强度
// console.log("hit", e);
const impactStrength = e.contact.getImpactVelocityAlongNormal();
console.log(impactStrength); // 获取碰撞的强度
if (impactStrength > 2) {
// 重新从零开始播放
hitSound.currentTime = 0;
hitSound.play();
}
}
sphereBody.addEventListener("collide", HitEvent);
無料音楽素材ダウンロードアドレスAigei.com:https://www.aigei.com/
import * as THREE from "three";
// 导入轨道控制器
import {
OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 导入动画库
import gsap from "gsap";
// 导入dat.gui
import * as dat from "dat.gui";
// 导入connon引擎
import * as CANNON from "cannon-es";
// 目标:使用cannon引擎
console.log(CANNON);
// const gui = new dat.GUI();
// 1、创建场景
const scene = new THREE.Scene();
// 2、创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
300
);
// 设置相机位置
camera.position.set(0, 0, 18);
scene.add(camera);
// 创建球和平面
const sphereGeometry = new THREE.SphereGeometry(1, 20, 20);
const sphereMaterial = new THREE.MeshStandardMaterial();
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.castShadow = true;
scene.add(sphere);
const floor = new THREE.Mesh(
new THREE.PlaneBufferGeometry(20, 20),
new THREE.MeshStandardMaterial()
);
floor.position.set(0, -5, 0);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
// 创建物理世界
// const world = new CANNON.World({ gravity: 9.8 });
const world = new CANNON.World();
world.gravity.set(0, -9.8, 0);
// 创建物理小球形状
const sphereShape = new CANNON.Sphere(1);
//设置物体材质
const sphereWorldMaterial = new CANNON.Material("sphere");
// 创建物理世界的物体
const sphereBody = new CANNON.Body({
shape: sphereShape,
position: new CANNON.Vec3(0, 0, 0),
// 小球质量
mass: 1,
// 物体材质
material: sphereWorldMaterial,
});
// 将物体添加至物理世界
world.addBody(sphereBody);
// 创建击打声音
const hitSound = new Audio("assets/metalHit.mp3");
// 添加监听碰撞事件
function HitEvent(e) {
// 获取碰撞的强度
// console.log("hit", e);
const impactStrength = e.contact.getImpactVelocityAlongNormal();
console.log(impactStrength); // 获取碰撞的强度
if (impactStrength > 2) {
// 重新从零开始播放
hitSound.currentTime = 0;
hitSound.play();
}
}
sphereBody.addEventListener("collide", HitEvent);
// 物理世界创建地面
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
const floorMaterial = new CANNON.Material("floor");
floorBody.material = floorMaterial;
// 当质量为0的时候,可以使得物体保持不动
floorBody.mass = 0;
floorBody.addShape(floorShape);
// 地面位置
floorBody.position.set(0, -5, 0);
// 旋转地面的位置
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(floorBody);
// 设置2种材质碰撞的参数
const defaultContactMaterial = new CANNON.ContactMaterial(
sphereMaterial,
floorMaterial,
{
// 摩擦力
friction: 0.1,
// 弹性
restitution: 0.7,
}
);
// 讲材料的关联设置添加的物理世界
world.addContactMaterial(defaultContactMaterial);
//添加环境光和平行光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.castShadow = true;
scene.add(dirLight);
// 初始化渲染器
// 渲染器透明
const renderer = new THREE.WebGLRenderer({
alpha: true });
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;
// console.log(renderer);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);
// // 使用渲染器,通过相机将场景渲染进来
// renderer.render(scene, camera);
// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
controls.enableDamping = true;
// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 设置时钟
const clock = new THREE.Clock();
function render() {
// let time = clock.getElapsedTime();
let deltaTime = clock.getDelta();
// 更新物理引擎里世界的物体
world.step(1 / 120, deltaTime);
sphere.position.copy(sphereBody.position);
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}
render();
// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
// console.log("画面变化了");
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比
renderer.setPixelRatio(window.devicePixelRatio);
});
4. 摩擦係数と弾性係数を設定する関連材料
//设置物体材质
const cubeWorldMaterial = new CANNON.Material("cube");
// 设置地面材质
const floorMaterial = new CANNON.Material("floor");
floorBody.material = floorMaterial;
// 设置2种材质碰撞的参数
const defaultContactMaterial = new CANNON.ContactMaterial(
cubeWorldMaterial,
floorMaterial,
{
// 摩擦力
friction: 0.1,
// 弹性
restitution: 0.7,
}
);
// 讲材料的关联设置添加的物理世界
world.addContactMaterial(defaultContactMaterial);
// 设置世界碰撞的默认材料,如果材料没有设置,都用这个
world.defaultContactMaterial = defaultContactMaterial;
5. 立方体同士が衝突した後の回転効果
// ウィンドウがクリックされるたびにオブジェクトを作成します
window.addEventListener("click", createCube);
const cubeArr = [];
//设置物体材质
const cubeWorldMaterial = new CANNON.Material("cube");
function createCube() {
// 创建立方体和平面
const cubeGeometry = new THREE.BoxBufferGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshStandardMaterial();
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.castShadow = true; //阴影
scene.add(cube);
// 创建物理cube形状
const cubeShape = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5));
// 创建物理世界的物体
const cubeBody = new CANNON.Body({
shape: cubeShape,
position: new CANNON.Vec3(0, 0, 0),
// 小球质量
mass: 1,
// 物体材质
material: cubeWorldMaterial,
});
cubeBody.applyLocalForce(
new CANNON.Vec3(300, 0, 0), //添加的力的大小和方向
new CANNON.Vec3(0, 0, 0) //施加的力所在的位置
);
// 将物体添加至物理世界
world.addBody(cubeBody);
// 添加监听碰撞事件
function HitEvent(e) {
// 获取碰撞的强度
// console.log("hit", e);
const impactStrength = e.contact.getImpactVelocityAlongNormal();
console.log(impactStrength);
if (impactStrength > 2) {
// 重新从零开始播放
hitSound.currentTime = 0;
hitSound.volume = impactStrength / 12;
hitSound.play();
}
}
cubeBody.addEventListener("collide", HitEvent);
cubeArr.push({
mesh: cube,
body: cubeBody,
});
}
//オブジェクトは落下後に回転します
cubeArr.forEach((item) => {
item.mesh.position.copy(item.body.position);
// 设置渲染的物体跟随物理的物体旋转
item.mesh.quaternion.copy(item.body.quaternion);
});
衝撃後徐々に音が小さくなった
// 添加监听碰撞事件
function HitEvent(e) {
// 获取碰撞的强度
// console.log("hit", e);
const impactStrength = e.contact.getImpactVelocityAlongNormal();
console.log(impactStrength);
if (impactStrength > 2) {
// 重新从零开始播放
hitSound.currentTime = 0;
hitSound.volume = impactStrength / 12;
hitSound.play();
}
}
cubeBody.addEventListener("collide", HitEvent);
6. 物体に力を加える
cubeBody.applyLocalForce(
new CANNON.Vec3(300, 0, 0), //添加的力的大小和方向
new CANNON.Vec3(0, 0, 0) //施加的力所在的位置
);
import * as THREE from "three";
// 导入轨道控制器
import {
OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 导入动画库
import gsap from "gsap";
// 导入dat.gui
import * as dat from "dat.gui";
// 导入connon引擎
import * as CANNON from "cannon-es";
// 目标:设置cube跟着旋转
console.log(CANNON);
// const gui = new dat.GUI();
// 1、创建场景
const scene = new THREE.Scene();
// 2、创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
300
);
// 设置相机位置
camera.position.set(0, 0, 18);
scene.add(camera);
const cubeArr = [];
//设置物体材质
const cubeWorldMaterial = new CANNON.Material("cube");
function createCube() {
// 创建立方体和平面
const cubeGeometry = new THREE.BoxBufferGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshStandardMaterial();
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.castShadow = true; //阴影
scene.add(cube);
// 创建物理cube形状
const cubeShape = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5));
// 创建物理世界的物体
const cubeBody = new CANNON.Body({
shape: cubeShape,
position: new CANNON.Vec3(0, 0, 0),
// 小球质量
mass: 1,
// 物体材质
material: cubeWorldMaterial,
});
cubeBody.applyLocalForce(
new CANNON.Vec3(300, 0, 0), //添加的力的大小和方向
new CANNON.Vec3(0, 0, 0) //施加的力所在的位置
);
// 将物体添加至物理世界
world.addBody(cubeBody);
// 添加监听碰撞事件
function HitEvent(e) {
// 获取碰撞的强度
// console.log("hit", e);
const impactStrength = e.contact.getImpactVelocityAlongNormal();
console.log(impactStrength);
if (impactStrength > 2) {
// 重新从零开始播放
hitSound.currentTime = 0;
hitSound.volume = impactStrength / 12;
hitSound.play();
}
}
cubeBody.addEventListener("collide", HitEvent);
cubeArr.push({
mesh: cube,
body: cubeBody,
});
}
window.addEventListener("click", createCube);
// 平面
const floor = new THREE.Mesh(
new THREE.PlaneBufferGeometry(20, 20),
new THREE.MeshStandardMaterial()
);
floor.position.set(0, -5, 0);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true; // 接收阴影
scene.add(floor);
// 创建物理世界
// const world = new CANNON.World({ gravity: 9.8 });
const world = new CANNON.World();
world.gravity.set(0, -9.8, 0); // 重力方向
// 创建击打声音
const hitSound = new Audio("assets/metalHit.mp3");
// 物理世界创建地面
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
// 设置地面材质
const floorMaterial = new CANNON.Material("floor");
floorBody.material = floorMaterial;
// 当质量为0的时候,可以使得物体保持不动
floorBody.mass = 0;
floorBody.addShape(floorShape);
// 地面位置
floorBody.position.set(0, -5, 0);
// 旋转地面的位置
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(floorBody);
// 设置2种材质碰撞的参数
const defaultContactMaterial = new CANNON.ContactMaterial(
cubeWorldMaterial,
floorMaterial,
{
// 摩擦力
friction: 0.1,
// 弹性
restitution: 0.7,
}
);
// 讲材料的关联设置添加的物理世界
world.addContactMaterial(defaultContactMaterial);
// 设置世界碰撞的默认材料,如果材料没有设置,都用这个
world.defaultContactMaterial = defaultContactMaterial;
//添加环境光和平行光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.castShadow = true; // 投放阴影
scene.add(dirLight);
// 初始化渲染器
// 渲染器透明
const renderer = new THREE.WebGLRenderer({
alpha: true });
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;
// console.log(renderer);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);
// // 使用渲染器,通过相机将场景渲染进来
// renderer.render(scene, camera);
// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
controls.enableDamping = true;
// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 设置时钟
const clock = new THREE.Clock();
function render() {
// let time = clock.getElapsedTime();
let deltaTime = clock.getDelta();
// 更新物理引擎里世界的物体
world.step(1 / 120, deltaTime); // 更新
// cube.position.copy(cubeBody.position);
cubeArr.forEach((item) => {
item.mesh.position.copy(item.body.position);
// 设置渲染的物体跟随物理的物体旋转
item.mesh.quaternion.copy(item.body.quaternion);
});
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}
render();
// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
// console.log("画面变化了");
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比
renderer.setPixelRatio(window.devicePixelRatio);
});
7. 大砲流体シミュレーション
流体シミュレーションは、流体の動きや形状をシミュレートするために使用される技術であり、コンピューター グラフィックス、アニメーション、物理学、流体力学などの分野で一般的に使用されています。
コンピュータ グラフィックスでは、流体シミュレーションを使用して、さまざまなリアルな水、火、煙、雲、その他の効果を作成できます。波、川、炎、煙、雨、雪など。
アニメーション制作では、流体シミュレーションを使用して、キャラクターの服装、髪、ヘアスタイルなどのエフェクトや、爆発、炎などの特殊効果アニメーションを作成できます。
物理学や流体力学では、流体シミュレーションを使用して、水の流れ、空気の流れ、炎などの流体運動の法則を研究でき、工学設計やシミュレーションに非常に役立ちます。
ゲーム開発では、流体シミュレーションを使用してゲーム内に水、火、煙、その他のエフェクトを作成し、ゲーム画面をよりリアルにすることができます。
流体シミュレーション技術は、医療画像処理、繊維産業、油圧機械、化学工学などの他の分野にも応用できます。
SPH (Smooth Particle Hydrodynamics) は、流体の動きや形状をシミュレートできる流体シミュレーション アルゴリズムです。SPH アルゴリズムの中心的な考え方は、流体を多くの粒子に分割し、これらの粒子を使用して流体の動きをシミュレートすることです。
SPH アルゴリズムの主な手順は次のとおりです。
- 初期化: 初期状態では、流体は多くの粒子に分割され、各粒子の密度、速度、位置が計算されます。
- 力学量の計算: 流体の物理的特性と、流体周囲の粒子の密度、速度、位置に基づいて、加速度、速度、位置などの各粒子の機械量が各瞬間に計算されます。
- パーティクルの状態を更新: 機械的量に基づいてパーティクルの速度と位置を更新します。
- レンダリング: パーティクルの位置と密度を流体の形状にレンダリングします。
SPH アルゴリズムには、さまざまな流体 (気体や液体など)、多相流体、多孔質媒体、粘性流体、非ニュートン流体などのシミュレーションなど、多くの最適化と変形があります。
実際のアプリケーションでは、SPH アルゴリズムが解決する必要がある問題は非常に複雑であり、数学と物理学の十分な理解が必要であることに注意してください。この知識を初めて使用する場合は、いくつかの基本を学ぶ必要があるかもしれません。
Cannon.js は、WebGL と JavaScript をサポートするオープンソースの物理エンジンです。Cannon.js の SPHSystem クラスは、流体をシミュレートするために使用されるクラスであり、SPH アルゴリズムを使用します。
Cannon.js の SPHSystem クラスを使用すると、WebGL で流体を簡単にシミュレートできます。SPHSystem クラスの使用方法を示す簡単な例を次に示します。
// 创建一个SPH系统
var sph = new CANNON.SPHSystem();
// 添加一些粒子
for (var i = 0; i < 100; i++) {
var p = new CANNON.SPHSystem.Particle();
p.position.set(Math.random(), Math.random(), Math.random());
sph.addParticle(p);
}
// 每一帧更新粒子状态
function update() {
sph.step();
requestAnimationFrame(update);
}
update();
これにより、WebGL で流体をシミュレートできるようになります。パーティクルのレンダリングにも WebGL を使用する必要があり、目的の効果を達成するには対応するパラメータを設定する必要があることに注意してください。
CANNON.SPHSystem は、流体の密度、粘度、抵抗、その他のパラメータの設定、粒子への力の適用、障害物の追加などもサポートしています。実際のニーズに応じて、対応するパラメータを設定する必要があります。
CANNON.SPHSystem クラスには、流体のパフォーマンスを制御するためのプロパティがいくつかあります。これらの 4 つのプロパティは次のとおりです:
● 密度: 流体の粘度と抵抗を制御するために使用される流体の密度。
● パーティクル : すべてのパーティクルを含む配列。
● smoothingRadius: 粒子間の相互作用の距離を制御するために使用され、粒子間の距離がこの値より小さい場合、粒子間に影響が生じます。
● 粘度: 流体の粘度。流体の抵抗を制御するために使用されます。
中でも密度と粘度は流体の挙動を制御する重要なパラメータです。密度が高くなるほど、流体の粘性が高くなり、抵抗が大きくなります。粘度が高くなるほど、流体の抵抗が大きくなり、動きにくくなります。
パーティクル アトリビュートはパーティクルの配列であり、これを介して位置、速度などのパーティクルのプロパティにアクセスできます。
smoothingRadius は相互作用半径です。この値が大きいほど粒子間の相互作用が広くなり、小さいほど粒子間の相互作用が狭くなります。実際のニーズに応じて調整してください。
リアルな流体効果をシミュレートしたい場合は、実際のシーンに応じてこれらのパラメータを調整する必要があります。
8. 車両シミュレーション
RaycastVehicle
RaycastVehicle は、cannon.js によって提供される車両オブジェクトです。そして公式は私たちにデモを提供してくれました。
Raycast が RaycastVehicle と呼ばれるのはなぜですか
? これは、オブジェクトの物理シミュレーション原理に関連しています。このオブジェクトは剛体(CANNON.Body)を本体とし、剛体の四隅から下方向に固定長の光線を放射し、その光線と地面との交点を車両との接触点としています。このとき、ボディ剛体には前後方向が作用し、サスペンションの弾性と横方向のトラクション摩擦が発生します。
その中心となる計算はupdateVehicleメソッド内にあります。
説明書
使用方法については、公式の使用例を参照してください。
1. まず、RaycastVehicle オブジェクトを構築します。
vehicle = new CANNON.RaycastVehicle({
chassisBody: chassisBody,
indexRightAxis: 0,
indexForwardAxis: 2,
indexUpAxis: 1,
});
vehicle.addToWorld(world);
このうち、chassisBody はボディを表す剛体です。IndexRightAxis、indexForwardAxis、indexUpAxis は公式の例では使用されていません。それぞれ車の右軸、前軸、上軸を表し、0、1、2 はそれぞれ x、y、z 軸です。
2. 論理ホイールを追加します。
vehicle.addWheel(options);
オプション オブジェクトは、ホイールとサスペンションのパラメータです。
chassisConnectionPointLocal: new Vec3(),// 车轮连接点,相对于chassisBody(也是发射射线的起点)
directionLocal: new Vec3(),// 车轮的下方方向(垂直车身向下)
axleLocal: new Vec3(),// 车轴方向
suspensionRestLength: 1,// 悬挂长度(未受任何力)
suspensionMaxLength: 2,// 悬挂最大长度,限制计算出的suspensionLength
suspensionStiffness: 100,// 悬挂刚度
dampingCompression: 10,// 悬挂压缩阻尼
dampingRelaxation: 10,// 悬挂复原阻尼
maxSuspensionForce: Number.MAX_VALUE, // 限制计算出的suspensionForce
maxSuspensionTravel: 1,// 悬挂可伸长或压缩的最大距离
radius: 1,// 车轮半径
frictionSlip: 10000,// 滑动摩檫系数(用于计算车轮所能提供的最大摩檫力)
rollInfluence: 0.01,// 施加侧向力时的位置系数,越小越接近车身,防止侧翻
ホイール衝突を追加しました。論理ホイールを追加した後、衝突を生成するために剛体をワールドに追加する必要もあります。
cannon-es の RaycastVehicle クラスでは、ホイールを設定するオブジェクトには次のプロパティが含まれています。
- radius: ホイールの半径。ホイールのサイズを示します。
- directLocal: ホイール方向のローカル座標。本体に対するホイールの向きを指定します。
- サスペンション剛性: サスペンションの剛性。サスペンション システムの硬さを指定し、ホイールの弾性に影響します。
- suspensionRestLength: サスペンション システムの静的な長さ。無負荷時のサスペンション システムの長さを指定します。
- frictionSlip: 滑り摩擦。ホイールと地面の間の滑り摩擦を指定します。
- dampingRelaxation: 緩和ダンピング。ホイールのサスペンション効果に影響を与えるサスペンション システムの緩和ダンピングを指定します。
- dumpingCompression: 圧縮ダンピング。サスペンション システムの圧縮ダンピングを指定し、ホイールのサスペンション効果に影響します。
- maxSuspensionForce: 最大サスペンション力。サスペンション システムが耐えることができる最大サスペンション力を指定します。
- rollInfluence: ローリング インフルエンス。ホイールのローリング インフルエンスを指定し、車両のローリング エフェクトに影響を与えます。
- axleLocal: 軸のローカル座標。本体に対する軸の位置を指定します。
- chassisConnectionPointLocal: 車体接続点のローカル座標。ホイールと車体の間の接続点の位置を指定します。
- maxSuspensionTravel: サスペンションの最大移動量。サスペンション システムが移動できる最大距離、つまり車輪が上下に移動できる最大距離を指定します。このプロパティは、ホイールの移動範囲を制限するために使用され、ホイールの物理的な動作をシミュレートするのに役立ちます。
for (var i = 0; i < vehicle.wheelInfos.length; i++) {
var wheel = vehicle.wheelInfos[i];
var cylinderShape = new CANNON.Cylinder(wheel.radius, wheel.radius, wheel.radius / 2, 20);
var wheelBody = new CANNON.Body({
mass: 0
});
wheelBody.type = CANNON.Body.KINEMATIC;
wheelBody.collisionFilterGroup = 0; // turn off collisions
var q = new CANNON.Quaternion();
q.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), Math.PI / 2);// 把竖着的圆柱体放倒作为车轮
wheelBody.addShape(cylinderShape, new CANNON.Vec3(), q);
wheelBodies.push(wheelBody);
world.addBody(wheelBody);
ホイール衝突の場合は、トランザムもリアルタイムで更新します。
world.addEventListener('postStep', function () {
for (var i = 0; i < vehicle.wheelInfos.length; i++) {
var t = vehicle.wheelInfos[i].worldTransform;
var wheelBody = wheelBodies[i];
wheelBody.position.copy(t.position);
wheelBody.quaternion.copy(t.quaternion);
}
});
車両制御。RaycastVehicle の次の機能を使用して車両を制御できます。
applyEngineForce = function(value, wheelIndex) // 施加牵引力
setSteeringValue = function(value, wheelIndex) // 设置转向角(弧度)
setBrake = function(brake, wheelIndex) // 刹车
アニメーションを実装します。各フレームをレンダリングする前に、ボディと車輪のトランザムをthree.jsまたはBayalon.jsグラフィックスにコピーして、アニメーションを実現します。
ドリフトを実装する
レース ゲームでは、ドリフトによってゲームのエンターテイメント性が大幅に向上します。ドリフトを理解するには、まず車のステアリングを理解する必要があります。
後輪がスリップせずに操舵している場合、前輪と後輪の瞬間的な中心が操舵中心となり、
スリップしていない状態では操舵中心は静止し、車両は操舵中心に沿って円運動をすることになります。ドリフト中はステアリング中心も円を描き、車両の動きの円周はノンスキッドステアリング時の半径よりも小さくなります。ドリフトを実現するには、
ユーザーがドリフトを押した後に後輪の摩擦係数を変更できます。鍵:
vehicle.wheelInfos[2].frictionSlip= up ? 3.5: 1.5;
vehicle.wheelInfos[3].frictionSlip= up ? 3.5: 1.5;
ただし、このとき過度にドリフトしやすく、車が旋回してしまうことがあります。
回転半径については前回の記事で紹介しましたが、ここでも再度使用します。ステアリング半径を r、ホイールベースを l、ホイールベースを w とすると、
アッカーマン条件によれば、2 つの車輪のステアリング角度は次のようになります。
r = 6 + Math.abs(vehicle.currentVehicleSpeedKmHour) / 10
switch (event.keyCode) {
// 。。。
case 39: // right
vehicle.setSteeringValue(up ? 0 : -Math.atan(2 / (r + 1 / 2)), 0);
vehicle.setSteeringValue(up ? 0 : -Math.atan(2 / (r - 1 / 2)), 1);
break;
case 37: // left
vehicle.setSteeringValue(up ? 0 : Math.atan(2 / (r - 1 / 2)), 0);
vehicle.setSteeringValue(up ? 0 : Math.atan(2 / (r + 1 / 2)), 1);
break;
case 67:
vehicle.wheelInfos[2].frictionSlip = up ? 3.5 : 1.4;
vehicle.wheelInfos[3].frictionSlip = up ? 3.5 : 1.4;
さらに、カート ゲームでは、参考文献 [1] の方法を使用して、車体に横方向の力を加えて車両をよりスムーズにドリフトさせることもできます。