threejs(9) – Physik-Engine anwenden, um Objektinteraktionen einzurichten

1. Verstehen Sie die Physik-Engine und die Kanoneninstallation

Cannon.js ist eine Open-Source-3D-Physik-Engine zum Erstellen von 3D-Physiksimulationen in WebGL. Es bietet eine flexible API, die auf viele WebGL-Szenarien angewendet werden kann.

Wir müssen einige grundlegende Konzepte verstehen, darunter physische Einheiten, Kollisionen, physische Transformationen, Einschränkungen zwischen der physischen Welt und physischen Einheiten usw.

Fügen Sie hier eine Bildbeschreibung ein

Offizielle Website: https://pmndrs.github.io/cannon-es/

npm: https://www.npmjs.com/package/cannon-es

npm i --save cannon-es

cannon-es ist eine leichte und benutzerfreundliche Netzwerk-3D-Physik-Engine. Es ist von der einfachen API von three.js inspiriert und basiert auf ammo.js und der Bullet-Physik-Engine.
Das erste, was wir einrichten müssen, ist unsere Physikwelt, die alle unsere Physikeinheiten beherbergen und die Simulation vorantreiben wird.
Lasst uns eine Welt erschaffen, die die Schwerkraft der Erde nutzt. Beachten Sie, dass cannon.js SI-Einheiten (Meter, Kilogramm, Sekunden usw.) verwendet.

const world = new CANNON.World({
    
    
  gravity: new CANNON.Vec3(0, -9.82, 0), // m/s²
})

Um die Simulation voranzutreiben, müssen wir world.fixedStep() in jedem Frame aufrufen. Als erstes Argument können wir einen festen Zeitschritt übergeben, in dem die Simulation ausgeführt werden soll. Der Standardwert ist 1/60, was 60 fps bedeutet. world.fixedStep() verfolgt die zuletzt aufgerufene Simulation, um unabhängig von der Bildrate die gleiche Geschwindigkeit beizubehalten, da requestAnimationFrame-Aufrufe je nach Gerät variieren können oder wenn Leistungsprobleme vorliegen. Lesen Sie hier mehr über festes analoges Stepping.

function animate() {
    
    
  requestAnimationFrame(animate)

  // Run the simulation independently of framerate every 1 / 60 ms
  world.fixedStep()
}
// Start the simulation loop
animate()

Wenn Sie die Zeit seit dem letzten Aufruf (dt. in der Spielwelt) vertreiben möchten, können Sie die erweiterte Funktion world.step() verwenden.
Sehen Sie sich das Beispiel für erweitertes World-Stepping an

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()

Starre Körper sind Einheiten, die in der Welt simuliert werden. Sie können einfache Formen wie Kugel, Kasten, Ebene, Zylinder oder komplexere Formen wie konvexes Polyeder, Partikel, Höhenfeld oder Trimesh sein.
Lassen Sie uns eine Grundkugel erstellen.

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)

Wie Sie sehen, haben wir ein Massenattribut angegeben, und die Masse definiert, wie sich der Körper verhält, wenn Kräfte auf ihn einwirken.
Wenn Objekte eine Masse haben und durch Kräfte beeinflusst werden, werden sie dynamische Objekte genannt. Es gibt auch kinematische Einheiten, die nicht durch Kräfte beeinflusst werden, aber Geschwindigkeit haben und sich bewegen können. Die dritte Art von Objekten sind statische Objekte, die nur in der Welt positioniert werden können und nicht von Kräften oder Geschwindigkeiten beeinflusst werden.
Wenn Sie einem Objekt eine Masse von 0 übergeben, wird das Objekt automatisch als statisches Objekt markiert. Sie können den Körpertyp auch in den Körperoptionen angeben. Lassen Sie uns zum Beispiel einen statischen Boden erstellen.

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)

Nachfolgend sind alle bisherigen Snippets zu einem vollständigen Beispiel zusammengefasst.

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()

Beachten Sie, dass Cannon nicht dafür verantwortlich ist, etwas auf dem Bildschirm darzustellen, sondern nur die Mathematik für die Simulation berechnet. Um tatsächlich etwas auf dem Bildschirm anzuzeigen, müssen Sie eine Rendering-Bibliothek wie three.js verwenden. Mal sehen, wie wir das erreichen.
Zuerst müssen Sie die entsprechende Entität für body in three.js erstellen. So erstellen Sie beispielsweise eine Kugel in 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)

Dann müssen Sie das three.js-Raster mit dem cannon.js-Körper verbinden. Dazu müssen Sie in jedem Frame, während Sie durch die Welt gehen, die Positions- und Rotationsdaten vom Körper in das Netz kopieren.

function animate() {
    
    
  requestAnimationFrame(animate)

  // world stepping...

  sphereMesh.position.copy(sphereBody.position)
  sphereMesh.quaternion.copy(sphereBody.quaternion)

  // three.js render...
}
animate()

2. Verwenden Sie die Physik-Engine, um Threejs-Objekte zu verknüpfen

Fügen Sie hier eine Bildbeschreibung ein
Eine Physik-Engine und eine Rendering-Engine. Die Rendering-Engine erhält Daten von der Physik-Engine zum Rendern.
Fügen Sie hier eine Bildbeschreibung ein

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);

Wie die Physik-Engine mit der Rendering-Engine zusammenhängt

 // 更新物理引擎里世界的物体
  world.step(1 / 120, deltaTime); // 更新
  sphere.position.copy(sphereBody.position); // 渲染引擎复制物理引擎中的数据 做渲染 自由落体

freier Fall

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. Stellen Sie einen festen Boden ein, um mit dem Ball zusammenzustoßen

Wie stoppt ein Ball, nachdem er den Boden berührt hat?

Dann erschaffe auch einen physischen Weltgrund

// 物理世界创建地面
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);

Auf diese Weise stoppt der Ball, wenn er den Boden berührt.
Fügen Sie hier eine Bildbeschreibung ein

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. Überwachen Sie Kollisionsereignisse und steuern Sie Kollisionssoundeffekte

Fügen Sie hier eine Bildbeschreibung ein

// 创建击打声音
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);

Kostenlose Download-Adresse für Musikmaterial, 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. Zugehörige Materialeinstellungsreibung und Elastizitätskoeffizient

//设置物体材质
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. Rotationseffekt, nachdem Würfel miteinander kollidieren

//Erstelle jedes Mal ein Objekt, wenn auf das Fenster geklickt wird
Fügen Sie hier eine Bildbeschreibung ein

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,
  });
}

//Das Objekt dreht sich nach dem Fall

  cubeArr.forEach((item) => {
    
    
    item.mesh.position.copy(item.body.position);
    // 设置渲染的物体跟随物理的物体旋转
    item.mesh.quaternion.copy(item.body.quaternion);
  });

Nach dem Aufprall wurde das Geräusch allmählich schwächer

  // 添加监听碰撞事件
  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. Üben Sie Kraft auf Gegenstände aus

  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. Kanonenflüssigkeitssimulation

Fluidsimulation ist eine Technologie zur Simulation der Bewegung und Form von Flüssigkeiten und wird häufig in der Computergrafik, Animation, Physik, Strömungsmechanik und anderen Bereichen eingesetzt.
In der Computergrafik kann die Flüssigkeitssimulation verwendet werden, um eine Vielzahl realistischer Wasser-, Feuer-, Rauch-, Wolken- und anderer Effekte zu erzeugen. Wie Wellen, Flüsse, Flammen, Rauch, Regen und Schnee usw.
Bei der Animationsproduktion können mithilfe der Fluidsimulation Effekte wie Kleidung, Haare und Frisuren der Charaktere sowie Animationen mit Spezialeffekten wie Explosionen, Flammen usw. erstellt werden.
In der Physik und Strömungsmechanik kann die Strömungssimulation verwendet werden, um die Gesetze der Flüssigkeitsbewegung zu untersuchen, wie z. B. Wasserströmung, Luftströmung, Flamme usw., was für den technischen Entwurf und die Simulation von großem Nutzen ist.
Bei der Spieleentwicklung können mithilfe der Flüssigkeitssimulation Wasser-, Feuer-, Rauch- und andere Effekte im Spiel erzeugt werden, um den Spielbildschirm realistischer zu gestalten.
Die Fluidsimulationstechnologie kann auch auf andere Bereiche wie medizinische Bildgebung, Textilindustrie, hydraulische Maschinen, Chemieingenieurwesen usw. angewendet werden.
SPH (Smooth Particle Hydrodynamics) ist ein Flüssigkeitssimulationsalgorithmus, der die Bewegung und Form von Flüssigkeiten simulieren kann. Die Kernidee des SPH-Algorithmus besteht darin, die Flüssigkeit in viele Partikel aufzuteilen und diese Partikel dann zur Simulation der Bewegung der Flüssigkeit zu verwenden.
Die Hauptschritte des SPH-Algorithmus sind wie folgt:

  1. Initialisierung: Im Ausgangszustand wird die Flüssigkeit in viele Partikel aufgeteilt und die Dichte, Geschwindigkeit und Position jedes Partikels berechnet.
  2. Berechnung mechanischer Größen: Zu jedem Zeitpunkt werden die mechanischen Größen jedes Partikels, wie Beschleunigung, Geschwindigkeit und Position, basierend auf den physikalischen Eigenschaften der Flüssigkeit und der Dichte, Geschwindigkeit und Position der Partikel um die Flüssigkeit herum berechnet.
  3. Partikelstatus aktualisieren: Aktualisieren Sie die Geschwindigkeit und Position des Partikels basierend auf mechanischen Größen.
  4. Rendern: Rendern Sie die Position und Dichte von Partikeln in die Form einer Flüssigkeit.

Es gibt viele Optimierungen und Varianten des SPH-Algorithmus, z. B. die Simulation verschiedener Flüssigkeiten (z. B. Gase und Flüssigkeiten), mehrphasiger Flüssigkeiten, poröser Medien, viskoser Flüssigkeiten, nicht-Newtonscher Flüssigkeiten usw.

Es ist zu beachten, dass in praktischen Anwendungen die Probleme, die der SPH-Algorithmus lösen muss, sehr komplex sind und ein gutes Verständnis der Mathematik und Physik erfordern. Wenn Sie mit diesem Wissen noch nicht vertraut sind, müssen Sie möglicherweise einige Grundlagen erlernen.
Cannon.js ist eine Open-Source-Physik-Engine, die WebGL und JavaScript unterstützt. Die SPHSystem-Klasse in Cannon.js ist eine Klasse zur Simulation von Flüssigkeiten. Sie verwendet den SPH-Algorithmus.
Flüssigkeiten können in WebGL mithilfe der SPHSystem-Klasse in Cannon.js einfach simuliert werden. Hier ist ein einfaches Beispiel, das die Verwendung der SPHSystem-Klasse veranschaulicht:

// 创建一个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();

Dadurch ist es möglich, Flüssigkeiten in WebGL zu simulieren. Es ist zu beachten, dass WebGL auch zum Rendern von Partikeln verwendet werden muss und entsprechende Parameter eingestellt werden müssen, um den gewünschten Effekt zu erzielen.
CANNON.SPHSystem unterstützt auch die Einstellung der Dichte, Viskosität, des Widerstands und anderer Parameter der Flüssigkeit sowie das Ausüben von Kraft auf Partikel, das Hinzufügen von Hindernissen usw. Entsprechende Parameter müssen entsprechend den tatsächlichen Anforderungen eingestellt werden.
In der Klasse CANNON.SPHSystem gibt es einige Eigenschaften zur Steuerung der Leistung der Flüssigkeit. Diese vier Eigenschaften sind:
● Dichte: Die Dichte der Flüssigkeit, die zur Steuerung der Viskosität und des Widerstands der Flüssigkeit verwendet wird.
● Partikel: Array, das alle Partikel enthält.
● SmoothingRadius: Wird zur Steuerung des Interaktionsabstands zwischen Partikeln verwendet. Wenn der Abstand zwischen Partikeln kleiner als dieser Wert ist, besteht ein Einfluss zwischen ihnen.
● Viskosität: Die Viskosität einer Flüssigkeit, die zur Steuerung des Widerstands der Flüssigkeit verwendet wird.
Unter ihnen sind Dichte und Viskosität wichtige Parameter, die das Flüssigkeitsverhalten steuern. Je höher die Dichte, desto viskoser ist die Flüssigkeit und desto größer ist der Widerstand. Je höher die Viskosität, desto größer ist der Widerstand der Flüssigkeit und desto schwerer lässt sie sich bewegen.
Das Attribut „Partikel“ ist ein Array von Partikeln, über das Sie auf die Eigenschaften von Partikeln wie Position, Geschwindigkeit usw. zugreifen können.
SmoothingRadius ist der Interaktionsradius. Je größer dieser Wert ist, desto breiter ist die Interaktion zwischen den Partikeln. Je kleiner er ist, desto enger ist die Interaktion zwischen den Partikeln. Passen Sie ihn entsprechend den tatsächlichen Anforderungen an.
Wenn Sie einen realistischen Flüssigkeitseffekt simulieren möchten, müssen Sie diese Parameter entsprechend der tatsächlichen Szene anpassen.

8. Fahrzeugsimulation

RaycastVehicle
RaycastVehicle ist ein von cannon.js bereitgestelltes Fahrzeugobjekt. Und der Beamte hat uns eine Demo zur Verfügung gestellt.

Warum
heißt Raycast RaycastVehicle? Dies hängt mit dem physikalischen Simulationsprinzip des Objekts zusammen. Dieses Objekt verwendet einen starren Körper (CANNON.Body) als Körper. Es sendet Strahlen fester Länge von den vier Ecken des starren Körpers nach unten aus. Der Schnittpunkt des Strahls und des Bodens wird als Kontaktpunkt zwischen dem Fahrzeug und verwendet Der Boden. An diesem Punkt wird eine Längsrichtung auf den Körper starrer Körper angewendet. Aufhängungselastizität und seitliche Traktionsreibung.

Fügen Sie hier eine Bildbeschreibung ein
Die Kernberechnung erfolgt in der updateVehicle- Methode.

Anweisungen

Informationen zur Verwendung finden Sie in den offiziellen Beispielen .
1. Erstellen Sie zunächst ein RaycastVehicle-Objekt:

vehicle = new CANNON.RaycastVehicle({
    
    
  chassisBody: chassisBody,
  indexRightAxis: 0,
  indexForwardAxis: 2,
  indexUpAxis: 1,
});
vehicle.addToWorld(world);

Unter diesen ist ChassisBody ein starrer Körper, der die Karosserie darstellt. IndexRightAxis, indexForwardAxis und indexUpAxis werden in den offiziellen Beispielen nicht verwendet. Sie repräsentieren die rechte, vordere und obere Achse des Autos und werden durch 0, 1 und 2 repräsentiert die x-, y- und z-Achsen.
2. Logisches Rad hinzufügen:

vehicle.addWheel(options);

Das Optionsobjekt sind die Parameter der Räder und der Aufhängung:

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,// 施加侧向力时的位置系数,越小越接近车身,防止侧翻

Radkollision hinzugefügt. Nachdem Sie das logische Rad hinzugefügt haben, müssen Sie der Welt auch einen starren Körper hinzufügen, um eine Kollision zu erzeugen:
In der RaycastVehicle-Klasse von cannon-es enthält das Objekt, das das Rad setzt, die folgenden Eigenschaften:

  1. Radius: Der Radius des Rades, der die Größe des Rades angibt.
  2. DirectionLocal: Die lokale Koordinate der Radrichtung, die die Ausrichtung des Rades relativ zur Karosserie angibt.
  3. SuspensionStiffness: Federungssteifigkeit, gibt die Härte des Federungssystems an und beeinflusst die Elastizität der Räder.
  4. SuspensionRestLength: Die statische Länge des Aufhängungssystems. Sie gibt die Länge des Aufhängungssystems an, wenn keine Last vorhanden ist.
  5. FrictionSlip: Gleitreibung, spezifiziert die Gleitreibung zwischen Rad und Boden.
  6. dampingRelaxation: Entspannungsdämpfung, gibt die Entspannungsdämpfung des Federungssystems an, die sich auf die Federungswirkung des Rades auswirkt.
  7. dampingCompression: Druckstufendämpfung, gibt die Druckstufendämpfung des Federungssystems an und beeinflusst die Federungswirkung des Rades.
  8. maxSuspensionForce: Maximale Federungskraft, die die maximale Federungskraft angibt, der das Federungssystem standhalten kann.
  9. rollInfluence: Rolleinfluss, spezifiziert den Rolleinfluss des Rades und beeinflusst den Rolleffekt des Fahrzeugs.
  10. axisLocal: Die lokale Koordinate der Achse, die die Position der Achse relativ zum Körper angibt.
  11. ChassisConnectionPointLocal: Die lokalen Koordinaten des Karosserieverbindungspunkts, die die Position des Verbindungspunkts zwischen den Rädern und der Karosserie angeben.
  12. maxSuspensionTravel: Maximaler Federweg, gibt die maximale Distanz an, die das Federungssystem zurücklegen kann, also die maximale Strecke, die sich die Räder nach oben und unten bewegen können. Diese Eigenschaft wird verwendet, um den Bewegungsbereich des Rads zu begrenzen und hilft, das physikalische Verhalten des Rads zu simulieren.
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);

Aktualisieren Sie bei Radkollisionen auch den Transom in Echtzeit:

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);
        }
});

Fahrzeug Kontrolle. Sie können die folgenden Funktionen von RaycastVehicle zur Steuerung des Fahrzeugs nutzen:

applyEngineForce = function(value, wheelIndex) // 施加牵引力
setSteeringValue = function(value, wheelIndex) // 设置转向角(弧度)
setBrake = function(brake, wheelIndex) // 刹车

Animation implementieren. Kopieren Sie vor dem Rendern jedes Frames den Transom der Karosserie und der Räder in die Grafiken von three.js oder Bayalon.js, um eine Animation zu realisieren.

Fügen Sie hier eine Bildbeschreibung ein

Drift umsetzen

Bei Rennspielen kann Driften den Spielspaß enorm steigern. Um das Driften zu verstehen, müssen Sie zunächst die Lenkung eines Autos verstehen.
Wenn das Hinterrad ohne Schlupf lenkt, ist der momentane Mittelpunkt des Vorderrads und des Hinterrads das Lenkzentrum.
Im nicht schlupffreien Zustand bleibt das Lenkzentrum stationär und das Fahrzeug führt eine Kreisbewegung entlang des Lenkzentrums aus. Beim Driften bewegt sich das Lenkzentrum ebenfalls kreisförmig, und der Umfang der Fahrzeugbewegung ist kleiner als der Radius beim rutschfesten Lenken: Um Drift zu
Fügen Sie hier eine Bildbeschreibung ein
erreichen, kann der Reibungskoeffizient des Hinterrads geändert werden, nachdem der Benutzer auf Drift gedrückt hat Schlüssel:

vehicle.wheelInfos[2].frictionSlip= up ? 3.5: 1.5;
vehicle.wheelInfos[3].frictionSlip= up ? 3.5: 1.5;

Allerdings ist es zu diesem Zeitpunkt leicht, übermäßig zu driften, und das Auto macht einen Kreis:
Fügen Sie hier eine Bildbeschreibung ein
Der Wenderadius wurde im vorherigen Artikel eingeführt und wir werden ihn hier erneut verwenden. Angenommen, der Lenkradius ist r, der Radstand ist l und der Radstand ist w:
Fügen Sie hier eine Bildbeschreibung ein
Gemäß Ackermanns Bedingung beträgt der Lenkwinkel der beiden Räder:

Fügen Sie hier eine Bildbeschreibung ein

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;

Fügen Sie hier eine Bildbeschreibung ein

Darüber hinaus können Sie in Kart-Spielen auch die Methode in Referenz [1] verwenden, um seitliche Kräfte auf die Karosserie auszuüben, damit das Fahrzeug sanfter driftet.

Supongo que te gusta

Origin blog.csdn.net/woyebuzhidao321/article/details/134126316
Recomendado
Clasificación