threejs案例,与静态三角形网格的基本碰撞, 鼠标环顾四周并投球游戏
- 创建一个时钟对象:
const clock = new THREE.Clock();
这行代码创建了一个新的THREE.Clock
对象,它用于跟踪经过的时间。这在动画和物理模拟中很有用。
2. 创建场景:
const scene = new THREE.Scene();
这行代码创建了一个新的3D场景。所有的物体(如模型、灯光等)都会添加到这个场景中。
3. 设置场景的背景和雾:
scene.background = new THREE.Color( 0x88ccee );
scene.fog = new THREE.Fog( 0x88ccee, 0, 50 );
这里设置了场景的背景色为浅蓝色,并添加了一个雾效果,使远处的物体看起来更模糊。
4. 创建相机:
const camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 1000 );
camera.rotation.order = 'YXZ';
这行代码创建了一个新的透视相机,它定义了观察3D场景的角度和范围。70
是视野角度,window.innerWidth / window.innerHeight
定义了相机的宽高比,0.1
和1000
是相机的近裁剪面和远裁剪面。camera.rotation.order = 'YXZ';
设置了相机旋转的顺序。
5. 添加半球光:
const fillLight1 = new THREE.HemisphereLight( 0x8dc1de, 0x00668d, 1.5 );
fillLight1.position.set( 2, 1, 1 );
scene.add( fillLight1 );
这部分代码创建了一个半球光,并将其添加到场景中。半球光模拟了一个柔和的环境光,由一个天空色和一个地面色组成。
6. 添加方向光:
const directionalLight = new THREE.DirectionalLight( 0xffffff, 2.5 );
// ... (设置方向光的各种属性)
scene.add( directionalLight );
这部分代码创建了一个方向光,并设置了其颜色、强度、位置、阴影属性等。方向光是从一个特定方向照射的光,通常用于模拟太阳光。
7. 创建渲染器:
const container = document.getElementById( 'container' );const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.VSMShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
container.appendChild( renderer.domElement );
这部分代码首先获取页面上的container
元素,然后创建一个WebGL渲染器。渲染器用于在网页上显示3D场景。这里还设置了渲染器的抗锯齿、像素比、尺寸、阴影映射类型和色调映射。
8. 添加性能监控:
const stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '0px';
container.appendChild( stats.domElement );
这部分代码添加了一个性能监控器,用于显示渲染器的帧率和其他相关信息。
- 常量定义:
const GRAVITY = 30;
const NUM_SPHERES = 100;
const SPHERE_RADIUS = 0.2;
const STEPS_PER_FRAME = 5;
这里定义了一些常量,用于控制球体的数量、半径、重力加速度和每帧的模拟步骤数。
- 创建球体几何体和材质:
const sphereGeometry = new THREE.IcosahedronGeometry( SPHERE_RADIUS, 5 );
const sphereMaterial = new THREE.MeshLambertMaterial( { color: 0xdede8d } );
使用THREE.IcosahedronGeometry
创建了一个二十面体几何体,用于作为球体的基础形状。然后,定义了一个Lambert材质,这种材质适用于模拟非直接光照的情况,并设置了球体的颜色。
- 创建和添加球体到场景中:
const spheres = [];
let sphereIdx = 0;for ( let i = 0; i < NUM_SPHERES; i ++ ) {// 创建球体网格const sphere = new THREE.Mesh( sphereGeometry, sphereMaterial );sphere.castShadow = true;sphere.receiveShadow = true;// 将球体添加到场景中scene.add( sphere );// 将球体和其他相关数据添加到数组中spheres.push({mesh: sphere,collider: new THREE.Sphere( new THREE.Vector3( 0, - 100, 0 ), SPHERE_RADIUS ),velocity: new THREE.Vector3()});
}
这段代码循环创建了指定数量的球体,并将它们添加到场景中。每个球体都有一个与之关联的碰撞体(一个Three.js的球体),用于物理模拟,以及一个速度向量。
- 创建八叉树:
const worldOctree = new Octree();
八叉树(Octree)是一种用于3D空间分割的数据结构,常用于碰撞检测和空间索引。这里创建了一个新的八叉树实例,但代码中没有显示其如何使用。
- 创建玩家碰撞体和速度向量:
const playerCollider = new Capsule( new THREE.Vector3( 0, 0.35, 0 ), new THREE.Vector3( 0, 1, 0 ), 0.35 );
const playerVelocity = new THREE.Vector3();
const playerDirection = new THREE.Vector3();
这里创建了玩家的碰撞体(一个胶囊形状),速度向量和方向向量。注意,Capsule
可能是一个自定义类,不是Three.js库的一部分。
- 初始化其他变量:
let playerOnFloor = false;
let mouseTime = 0;
const keyStates = {};
const vector1 = new THREE.Vector3();
const vector2 = new THREE.Vector3();
const vector3 = new THREE.Vector3();
这些变量可能用于控制玩家的状态(如是否在地板上)、鼠标交互、键盘输入以及临时存储向量计算的结果。
- 键盘按键监听:
document.addEventListener( 'keydown', ( event ) => {keyStates[ event.code ] = true;
} );document.addEventListener( 'keyup', ( event ) => {keyStates[ event.code ] = false;
} );
这里为文档对象(整个页面)添加了两个事件监听器,分别用于处理键盘的keydown
和keyup
事件。keydown
事件在用户按下键时触发,keyup
则在键被释放时触发。event.code
提供了被按下或释放的键的标识符。keyStates
是一个对象,用于存储每个键的当前状态(按下或释放)。
- 鼠标按下监听:
container.addEventListener( 'mousedown', () => {document.body.requestPointerLock();mouseTime = performance.now();
} );
当在container
元素上按下鼠标时,这段代码请求将指针锁定到页面上,这样当鼠标移动时,鼠标指针将不再显示,而页面的其他部分也不会接收鼠标事件。mouseTime
变量存储了鼠标按下的时间,可能是为了计算后续鼠标移动的持续时间或速度。
- 鼠标释放监听:
document.addEventListener( 'mouseup', () => {if ( document.pointerLockElement !== null ) throwBall();
} );
当鼠标按钮释放时,如果指针已经被锁定(即document.pointerLockElement
不为null),则调用throwBall
函数。从这段代码看不出throwBall
函数的实现细节,但可以猜测它的作用是抛出某种对象(可能是一个球体)。
- 鼠标移动监听:
document.body.addEventListener( 'mousemove', ( event ) => {if ( document.pointerLockElement === document.body ) {camera.rotation.y -= event.movementX / 500;camera.rotation.x -= event.movementY / 500;}
} );
当鼠标在文档体上移动时,如果指针被锁定到文档体上,这段代码会更新相机的旋转。event.movementX
和event.movementY
分别表示鼠标在X和Y轴上的移动量。通过除以500,代码将鼠标的移动量转换为较小的相机旋转量,从而实现平滑的相机控制。
- 窗口大小调整监听:
window.addEventListener( 'resize', onWindowResize );function onWindowResize() {camera.aspect = window.innerWidth / window.innerHeight;camera.updateProjectionMatrix();renderer.setSize( window.innerWidth, window.innerHeight );
}
这段代码监听窗口的大小调整事件。当窗口大小变化时,它会调用onWindowResize
函数,该函数会更新相机的纵横比,并更新其投影矩阵。此外,它还调用renderer.setSize
来确保渲染器的大小与窗口的大小相匹配。
这段代码是用于处理三维场景中的球体和玩家(或相机)的交互和移动。具体来说,它定义了三个函数:throwBall
、playerCollisions
和updatePlayer
。以下是对这些函数的详细解析:
throwBall
函数
这个函数用于投掷一个球体(可能是一个游戏对象,如球)。
-
获取球体:
const sphere = spheres[ sphereIdx ];
从
spheres
数组中获取一个球体,sphereIdx
是当前的球体索引。 -
设置投掷方向:
camera.getWorldDirection( playerDirection ); sphere.collider.center.copy( playerCollider.end ).addScaledVector( playerDirection, playerCollider.radius * 1.5 );
首先获取相机的世界方向(玩家面向的方向),然后将球体的位置设置为玩家碰撞体的末端,并沿着玩家方向移动一定距离(玩家碰撞体半径的1.5倍)。
-
计算投掷力度:
const impulse = 15 + 30 * ( 1 - Math.exp( ( mouseTime - performance.now() ) * 0.001 ) );
这里计算投掷的力度(或冲量)。力度基于鼠标按下的时间(
mouseTime
)和当前时间(performance.now()
)的差值来计算。时间差越大,力度越大。 -
设置球体的速度和方向:
sphere.velocity.copy( playerDirection ).multiplyScalar( impulse ); sphere.velocity.addScaledVector( playerVelocity, 2 );
球体的速度被设置为玩家方向,并乘以计算出的力度。然后,再基于玩家的速度进行微调。
-
更新球体索引:
sphereIdx = ( sphereIdx + 1 ) % spheres.length;
更新球体索引,以便下一次投掷时可以使用下一个球体。
playerCollisions
函数
这个函数处理玩家(或相机的碰撞体)与场景中的其他物体之间的碰撞。
-
检测碰撞:
const result = worldOctree.capsuleIntersect( playerCollider );
使用
worldOctree
(可能是一个空间划分的数据结构,用于加速碰撞检测)检测玩家的碰撞体是否与任何物体相交。 -
判断是否在地面上:
playerOnFloor = false; if ( result ) {playerOnFloor = result.normal.y > 0;// ... }
如果发生碰撞,检查碰撞的法线(
result.normal
)的y分量是否大于0来判断玩家是否在地面上。 -
处理非地面碰撞:
if ( ! playerOnFloor ) {playerVelocity.addScaledVector( result.normal, - result.normal.dot( playerVelocity) ); }
如果玩家不在地面上,更新玩家的速度以反映碰撞的影响。
-
移动碰撞体以处理穿透:
playerCollider.translate( result.normal.multiplyScalar( result.depth) );
移动玩家的碰撞体以处理任何可能发生的穿透。
updatePlayer
函数
这个函数用于更新玩家的位置和速度。
-
计算阻尼:
let damping = Math.exp( - 4 * deltaTime ) - 1;
阻尼用于模拟玩家速度的逐渐减小(例如空气阻力或摩擦)。
-
处理玩家在空中的移动:
if ( ! playerOnFloor ) {playerVelocity.y -= GRAVITY * deltaTime;damping *= 0.1; }
如果玩家不在地面上,更新玩家的y速度以模拟重力,并减小阻尼。
-
更新玩家速度:
playerVelocity.addScaledVector( playerVelocity, damping );
根据阻尼更新玩家的速度。
-
移动玩家碰撞体:
const deltaPosition = playerVelocity.clone().multiplyScalar( deltaTime ); playerCollider.translate( deltaPosition );
计算玩家应该移动的距离,并更新玩家的碰撞体位置。
-
检测碰撞并更新相机位置:
playerCollisions(); camera.position.copy( playerCollider.end );
调用
playerCollisions
函数来处理可能的碰撞,然后将相机的位置设置为玩家碰撞体的末端。
playerSphereCollision
函数
这个函数处理玩家球体与另一个球体的碰撞。
-
计算玩家碰撞体的中心:
const center = vector1.addVectors( playerCollider.start, playerCollider.end ).multiplyScalar( 0.5 );
玩家碰撞体可能是一个胶囊体(capsule),这里计算其中心点。
-
获取球体的中心:
const sphere_center = sphere.collider.center;
-
计算碰撞半径:
const r = playerCollider.radius + sphere.collider.radius; const r2 = r * r;
这是玩家碰撞体和球体碰撞体半径之和,以及它的平方。
-
碰撞检测:
for ( const point of [ playerCollider.start, playerCollider.end, center ] ) {// ... }
这里将玩家碰撞体近似为三个点(开始点、结束点和中心点)进行碰撞检测。对于每个点,它计算到球体中心的距离平方,并检查这个距离是否小于两个碰撞体半径之和的平方。
-
碰撞响应:
如果发生碰撞,代码计算碰撞法线(normal),然后根据法线和两个物体的速度来计算碰撞后的新速度。同时,调整球体的位置以解决任何可能的穿透。
spheresCollisions
函数
这个函数处理球体之间的碰撞。
-
双层循环遍历球体:
for ( let i = 0, length = spheres.length; i < length; i ++ ) {// ...for ( let j = i + 1; j < length; j ++ ) {// ...} }
使用两个嵌套的循环来遍历所有球体对。这样可以确保每个球体对只被检查一次。
-
计算球体间的距离平方:
const d2 = s1.collider.center.distanceToSquared( s2.collider.center );
-
检查碰撞:
如果两个球体中心之间的距离小于它们半径之和,则发生碰撞。 -
碰撞响应:
类似于playerSphereCollision
函数,根据碰撞法线和两个球体的速度来计算新的速度,并调整球体的位置。
这段代码实现了三维空间中的碰撞检测和响应。它使用了简化的碰撞检测(将玩家碰撞体近似为三个点)和基于物理的碰撞响应(改变物体的速度和位置)。这种碰撞处理在实时渲染和物理模拟中非常常见,特别是在游戏开发中。
这段代码主要实现了三维空间中多个球体的更新和碰撞处理。以下是对代码的详细解析:
updateSpheres
函数
这个函数负责更新所有球体的位置和速度,并处理它们与场景中其他物体的碰撞。
-
更新球体位置:
sphere.collider.center.addScaledVector( sphere.velocity, deltaTime );
根据球体的速度和经过的时间(
deltaTime
)来更新球体的位置。 -
检查球体与场景中的碰撞:
const result = worldOctree.sphereIntersect( sphere.collider );
使用
worldOctree
(可能是一个用于空间划分的数据结构)来检查球体是否与场景中的其他物体发生碰撞。 -
处理碰撞:
如果发生碰撞(result
为真),则调整球体的速度和位置以响应碰撞。这里使用了基于物理的碰撞响应,通过沿着碰撞法线(result.normal
)反向推动球体来解决穿透问题。 -
处理重力:
如果球体没有碰撞到任何东西(result
为假),则球体的y轴速度(竖直方向)会受到重力的影响而减小。 -
速度阻尼:
const damping = Math.exp( - 1.5 * deltaTime ) - 1; sphere.velocity.addScaledVector( sphere.velocity, damping );
对球体的速度应用阻尼,使其逐渐减小。这可以模拟空气阻力或其他形式的能量损失。
-
检查与玩家的碰撞:
playerSphereCollision( sphere );
调用
playerSphereCollision
函数来检查球体是否与玩家发生碰撞,并相应地更新它们的速度和位置。 -
处理球体间的碰撞:
spheresCollisions();
调用
spheresCollisions
函数来处理球体之间的碰撞。 -
更新球体的视觉表示:
sphere.mesh.position.copy( sphere.collider.center );
将球体的视觉表示(
mesh
)的位置更新为其碰撞体的中心位置,以确保视觉和物理状态一致。
getForwardVector
函数
这个函数返回相机(或玩家)的前方向量。
-
获取相机的世界方向:
camera.getWorldDirection( playerDirection );
获取相机在世界坐标系中的方向。
-
调整方向并归一化:
playerDirection.y = 0; playerDirection.normalize();
将方向向量的y分量设为0(即忽略竖直方向),然后归一化向量。
getSideVector
函数
这个函数返回相机(或玩家)的侧向量。
-
获取相机的世界方向:
camera.getWorldDirection( playerDirection );
同样获取相机在世界坐标系中的方向。
-
计算侧向量:
playerDirection.cross( camera.up );
通过计算相机方向向量与上方向向量的叉积来得到侧向量。
-
玩家控制(
controls
函数):- 根据
deltaTime
(上一次渲染到当前渲染的时间差)和玩家是否在地面(playerOnFloor
)来设定玩家的移动速度(speedDelta
)。 - 根据按键状态(
keyStates
)来更新玩家的速度(playerVelocity
)。具体来说,如果按下’W’键,玩家会向前移动;如果按下’S’键,玩家会向后移动;如果按下’A’键,玩家会向左移动;如果按下’D’键,玩家会向右移动。 - 如果玩家在地面并且按下空格键,玩家的垂直速度(y轴)会设置为15,实现跳跃功能。
- 根据
-
模型加载和场景设置:
- 使用
GLTFLoader
来加载一个名为collision-world.glb
的3D模型。 - 将加载的模型(
gltf.scene
)添加到场景(scene
)中。 - 创建一个
worldOctree
数据结构来存储场景的碰撞信息,并通过gltf.scene
来初始化它。 - 遍历模型的每一个子节点,如果它是网格(
isMesh
),则设置它的阴影投射和接收属性,并优化其贴图的采样(通过设置anisotropy
)。 - 创建一个
OctreeHelper
对象,这是一个用于可视化worldOctree
的辅助对象,但默认是隐藏的。 - 使用
GUI
库来创建一个界面元素,用户可以通过这个界面来切换OctreeHelper
的可见性。
- 使用
-
玩家位置重置(
teleportPlayerIfOob
函数):- 如果相机(代表玩家)的位置在y轴上的值小于或等于-25(可能表示玩家掉出了世界边界),则将玩家的碰撞体(
playerCollider
)的位置和大小重置,并将相机位置设置为新的碰撞体位置,同时将相机的旋转重置。这实际上是将玩家“传送”回世界中的某个安全位置。
- 如果相机(代表玩家)的位置在y轴上的值小于或等于-25(可能表示玩家掉出了世界边界),则将玩家的碰撞体(
整体上,这段代码为3D场景中的玩家控制、模型加载和场景设置以及玩家位置重置提供了实现。其中,controls
函数负责根据玩家的输入更新玩家的速度,而模型加载和场景设置部分则通过加载一个3D模型并设置其相关属性来初始化场景。最后,teleportPlayerIfOob
函数提供了一个机制来确保玩家不会掉出世界的边界。
animate
函数是3D渲染应用中常见的动画循环函数,用于在每个动画帧中更新场景的状态并渲染场景。以下是对这段代码的详细解析:
代码功能概述
-
计算时间差(
deltaTime
): 通过clock.getDelta()
获取从上一次动画帧到现在的时间差(以秒为单位),并限制其最大值为0.05秒。之后,将这个时间差除以STEPS_PER_FRAME
,得到一个子步长的时间差。 -
碰撞检测的子步处理: 通过一个循环,执行
STEPS_PER_FRAME
次更新操作,每次循环中都会调用玩家控制和更新函数,以此来降低物体快速移动导致碰撞检测失败的风险。 -
更新函数:
controls( deltaTime )
: 根据当前的时间差deltaTime
更新玩家的速度和位置。updatePlayer( deltaTime )
: 更新玩家的状态或位置,可能还包括与环境的交互。updateSpheres( deltaTime )
: 更新场景中的球体(或其他物体)的状态。teleportPlayerIfOob()
: 如果玩家超出边界,则将其传送回安全位置。
-
渲染场景: 使用
renderer.render( scene, camera )
渲染当前的场景和相机视图。 -
更新性能统计:
stats.update()
可能用于更新一些性能统计信息,比如帧率等。 -
请求下一帧: 使用
requestAnimationFrame( animate )
请求浏览器的下一帧动画,并将animate
函数作为回调函数,从而形成一个连续的动画循环。
细节分析
-
deltaTime
的计算考虑了STEPS_PER_FRAME
,这通常用于减少快速移动物体错过碰撞检测的机会。通过将总的deltaTime
分割成多个子步,每个子步中更新物体的位置,可以提高碰撞检测的准确性。 -
controls
函数负责根据用户的输入和当前的时间差来更新玩家的移动状态。 -
updatePlayer
和updateSpheres
函数分别更新玩家和场景中球体的状态。这些更新可能包括位置、速度、动画状态等。 -
teleportPlayerIfOob
函数用于处理玩家超出边界的情况,确保玩家不会离开游戏世界。 -
renderer.render( scene, camera )
是渲染命令,它使用当前的场景和相机状态来生成图像。 -
stats.update()
可能是用于更新性能统计信息的,例如帧率、渲染时间等,这对于调试和优化非常有用。 -
requestAnimationFrame( animate )
确保animate
函数在每个浏览器动画帧被调用,从而保持动画的流畅性和同步性。
总结
这段代码定义了一个 animate
函数,它是3D应用中典型的动画循环。它首先计算时间差,并在多个子步中更新玩家和场景中物体的状态,然后进行渲染和性能统计的更新。最后,它使用 requestAnimationFrame
来请求下一帧的动画,从而形成一个连续的动画循环。这个循环确保了游戏或应用的流畅运行和实时交互。
全部源码
<!DOCTYPE html>
<html lang="en"><head><title>three.js - misc - octree collisions</title><meta charset=utf-8 /><meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"><link type="text/css" rel="stylesheet" href="main.css"></head><body><div id="info">Octree threejs demo - basic collisions with static triangle mesh<br />MOUSE to look around and to throw balls<br/>WASD to move and SPACE to jump</div><div id="container"></div><script type="importmap">{"imports": {"three": "../build/three.module.js","three/addons/": "./jsm/"}}</script><script type="module">import * as THREE from 'three';import Stats from 'three/addons/libs/stats.module.js';import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';import { Octree } from 'three/addons/math/Octree.js';import { OctreeHelper } from 'three/addons/helpers/OctreeHelper.js';import { Capsule } from 'three/addons/math/Capsule.js';import { GUI } from 'three/addons/libs/lil-gui.module.min.js';const clock = new THREE.Clock();const scene = new THREE.Scene();scene.background = new THREE.Color( 0x88ccee );scene.fog = new THREE.Fog( 0x88ccee, 0, 50 );const camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 1000 );camera.rotation.order = 'YXZ';const fillLight1 = new THREE.HemisphereLight( 0x8dc1de, 0x00668d, 1.5 );fillLight1.position.set( 2, 1, 1 );scene.add( fillLight1 );const directionalLight = new THREE.DirectionalLight( 0xffffff, 2.5 );directionalLight.position.set( - 5, 25, - 1 );directionalLight.castShadow = true;directionalLight.shadow.camera.near = 0.01;directionalLight.shadow.camera.far = 500;directionalLight.shadow.camera.right = 30;directionalLight.shadow.camera.left = - 30;directionalLight.shadow.camera.top = 30;directionalLight.shadow.camera.bottom = - 30;directionalLight.shadow.mapSize.width = 1024;directionalLight.shadow.mapSize.height = 1024;directionalLight.shadow.radius = 4;directionalLight.shadow.bias = - 0.00006;scene.add( directionalLight );const container = document.getElementById( 'container' );const renderer = new THREE.WebGLRenderer( { antialias: true } );renderer.setPixelRatio( window.devicePixelRatio );renderer.setSize( window.innerWidth, window.innerHeight );renderer.shadowMap.enabled = true;renderer.shadowMap.type = THREE.VSMShadowMap;renderer.toneMapping = THREE.ACESFilmicToneMapping;container.appendChild( renderer.domElement );const stats = new Stats();stats.domElement.style.position = 'absolute';stats.domElement.style.top = '0px';container.appendChild( stats.domElement );const GRAVITY = 30;const NUM_SPHERES = 100;const SPHERE_RADIUS = 0.2;const STEPS_PER_FRAME = 5;const sphereGeometry = new THREE.IcosahedronGeometry( SPHERE_RADIUS, 5 );const sphereMaterial = new THREE.MeshLambertMaterial( { color: 0xdede8d } );const spheres = [];let sphereIdx = 0;for ( let i = 0; i < NUM_SPHERES; i ++ ) {const sphere = new THREE.Mesh( sphereGeometry, sphereMaterial );sphere.castShadow = true;sphere.receiveShadow = true;scene.add( sphere );spheres.push( {mesh: sphere,collider: new THREE.Sphere( new THREE.Vector3( 0, - 100, 0 ), SPHERE_RADIUS ),velocity: new THREE.Vector3()} );}const worldOctree = new Octree();const playerCollider = new Capsule( new THREE.Vector3( 0, 0.35, 0 ), new THREE.Vector3( 0, 1, 0 ), 0.35 );const playerVelocity = new THREE.Vector3();const playerDirection = new THREE.Vector3();let playerOnFloor = false;let mouseTime = 0;const keyStates = {};const vector1 = new THREE.Vector3();const vector2 = new THREE.Vector3();const vector3 = new THREE.Vector3();document.addEventListener( 'keydown', ( event ) => {keyStates[ event.code ] = true;} );document.addEventListener( 'keyup', ( event ) => {keyStates[ event.code ] = false;} );container.addEventListener( 'mousedown', () => {document.body.requestPointerLock();mouseTime = performance.now();} );document.addEventListener( 'mouseup', () => {if ( document.pointerLockElement !== null ) throwBall();} );document.body.addEventListener( 'mousemove', ( event ) => {if ( document.pointerLockElement === document.body ) {camera.rotation.y -= event.movementX / 500;camera.rotation.x -= event.movementY / 500;}} );window.addEventListener( 'resize', onWindowResize );function onWindowResize() {camera.aspect = window.innerWidth / window.innerHeight;camera.updateProjectionMatrix();renderer.setSize( window.innerWidth, window.innerHeight );}function throwBall() {const sphere = spheres[ sphereIdx ];camera.getWorldDirection( playerDirection );sphere.collider.center.copy( playerCollider.end ).addScaledVector( playerDirection, playerCollider.radius * 1.5 );// throw the ball with more force if we hold the button longer, and if we move forwardconst impulse = 15 + 30 * ( 1 - Math.exp( ( mouseTime - performance.now() ) * 0.001 ) );sphere.velocity.copy( playerDirection ).multiplyScalar( impulse );sphere.velocity.addScaledVector( playerVelocity, 2 );sphereIdx = ( sphereIdx + 1 ) % spheres.length;}function playerCollisions() {const result = worldOctree.capsuleIntersect( playerCollider );playerOnFloor = false;if ( result ) {playerOnFloor = result.normal.y > 0;if ( ! playerOnFloor ) {playerVelocity.addScaledVector( result.normal, - result.normal.dot( playerVelocity ) );}playerCollider.translate( result.normal.multiplyScalar( result.depth ) );}}function updatePlayer( deltaTime ) {let damping = Math.exp( - 4 * deltaTime ) - 1;if ( ! playerOnFloor ) {playerVelocity.y -= GRAVITY * deltaTime;// small air resistancedamping *= 0.1;}playerVelocity.addScaledVector( playerVelocity, damping );const deltaPosition = playerVelocity.clone().multiplyScalar( deltaTime );playerCollider.translate( deltaPosition );playerCollisions();camera.position.copy( playerCollider.end );}function playerSphereCollision( sphere ) {const center = vector1.addVectors( playerCollider.start, playerCollider.end ).multiplyScalar( 0.5 );const sphere_center = sphere.collider.center;const r = playerCollider.radius + sphere.collider.radius;const r2 = r * r;// approximation: player = 3 spheresfor ( const point of [ playerCollider.start, playerCollider.end, center ] ) {const d2 = point.distanceToSquared( sphere_center );if ( d2 < r2 ) {const normal = vector1.subVectors( point, sphere_center ).normalize();const v1 = vector2.copy( normal ).multiplyScalar( normal.dot( playerVelocity ) );const v2 = vector3.copy( normal ).multiplyScalar( normal.dot( sphere.velocity ) );playerVelocity.add( v2 ).sub( v1 );sphere.velocity.add( v1 ).sub( v2 );const d = ( r - Math.sqrt( d2 ) ) / 2;sphere_center.addScaledVector( normal, - d );}}}function spheresCollisions() {for ( let i = 0, length = spheres.length; i < length; i ++ ) {const s1 = spheres[ i ];for ( let j = i + 1; j < length; j ++ ) {const s2 = spheres[ j ];const d2 = s1.collider.center.distanceToSquared( s2.collider.center );const r = s1.collider.radius + s2.collider.radius;const r2 = r * r;if ( d2 < r2 ) {const normal = vector1.subVectors( s1.collider.center, s2.collider.center ).normalize();const v1 = vector2.copy( normal ).multiplyScalar( normal.dot( s1.velocity ) );const v2 = vector3.copy( normal ).multiplyScalar( normal.dot( s2.velocity ) );s1.velocity.add( v2 ).sub( v1 );s2.velocity.add( v1 ).sub( v2 );const d = ( r - Math.sqrt( d2 ) ) / 2;s1.collider.center.addScaledVector( normal, d );s2.collider.center.addScaledVector( normal, - d );}}}}function updateSpheres( deltaTime ) {spheres.forEach( sphere => {sphere.collider.center.addScaledVector( sphere.velocity, deltaTime );const result = worldOctree.sphereIntersect( sphere.collider );if ( result ) {sphere.velocity.addScaledVector( result.normal, - result.normal.dot( sphere.velocity ) * 1.5 );sphere.collider.center.add( result.normal.multiplyScalar( result.depth ) );} else {sphere.velocity.y -= GRAVITY * deltaTime;}const damping = Math.exp( - 1.5 * deltaTime ) - 1;sphere.velocity.addScaledVector( sphere.velocity, damping );playerSphereCollision( sphere );} );spheresCollisions();for ( const sphere of spheres ) {sphere.mesh.position.copy( sphere.collider.center );}}function getForwardVector() {camera.getWorldDirection( playerDirection );playerDirection.y = 0;playerDirection.normalize();return playerDirection;}function getSideVector() {camera.getWorldDirection( playerDirection );playerDirection.y = 0;playerDirection.normalize();playerDirection.cross( camera.up );return playerDirection;}function controls( deltaTime ) {// gives a bit of air controlconst speedDelta = deltaTime * ( playerOnFloor ? 25 : 8 );if ( keyStates[ 'KeyW' ] ) {playerVelocity.add( getForwardVector().multiplyScalar( speedDelta ) );}if ( keyStates[ 'KeyS' ] ) {playerVelocity.add( getForwardVector().multiplyScalar( - speedDelta ) );}if ( keyStates[ 'KeyA' ] ) {playerVelocity.add( getSideVector().multiplyScalar( - speedDelta ) );}if ( keyStates[ 'KeyD' ] ) {playerVelocity.add( getSideVector().multiplyScalar( speedDelta ) );}if ( playerOnFloor ) {if ( keyStates[ 'Space' ] ) {playerVelocity.y = 15;}}}const loader = new GLTFLoader().setPath( './models/gltf/' );loader.load( 'collision-world.glb', ( gltf ) => {scene.add( gltf.scene );worldOctree.fromGraphNode( gltf.scene );gltf.scene.traverse( child => {if ( child.isMesh ) {child.castShadow = true;child.receiveShadow = true;if ( child.material.map ) {child.material.map.anisotropy = 4;}}} );const helper = new OctreeHelper( worldOctree );helper.visible = false;scene.add( helper );const gui = new GUI( { width: 200 } );gui.add( { debug: false }, 'debug' ).onChange( function ( value ) {helper.visible = value;} );animate();} );function teleportPlayerIfOob() {if ( camera.position.y <= - 25 ) {playerCollider.start.set( 0, 0.35, 0 );playerCollider.end.set( 0, 1, 0 );playerCollider.radius = 0.35;camera.position.copy( playerCollider.end );camera.rotation.set( 0, 0, 0 );}}function animate() {const deltaTime = Math.min( 0.05, clock.getDelta() ) / STEPS_PER_FRAME;// we look for collisions in substeps to mitigate the risk of// an object traversing another too quickly for detection.for ( let i = 0; i < STEPS_PER_FRAME; i ++ ) {controls( deltaTime );updatePlayer( deltaTime );updateSpheres( deltaTime );teleportPlayerIfOob();}renderer.render( scene, camera );stats.update();requestAnimationFrame( animate );}</script></body>
</html>
本内容来源于小豆包,想要更多内容请跳转小豆包 》
相关文章:
threejs案例,与静态三角形网格的基本碰撞, 鼠标环顾四周并投球游戏
创建一个时钟对象: const clock new THREE.Clock();这行代码创建了一个新的THREE.Clock对象,它用于跟踪经过的时间。这在动画和物理模拟中很有用。 2. 创建场景: const scene new THREE.Scene();这行代码创建了一个新的3D场景。所有的物体(如模型、灯…...

将FastSAM中的TextPrompt迁移到MobileSAM中
本博文简单介绍了SAM、FastSAM与MobileSAM,主要关注于TextPrompt功能的使用。从性能上看MobileSAM是最实用的,但其没有提供TextPrompt功能,故而参考FastSAM中的实现,在MobileSAM中嵌入TextPrompt类。并将TextPrompt能力嵌入到MobileSAM官方项目提供的gradio.py部署代码中,…...
KY191 矩阵幂(用Java实现)
描述 给定一个n*n的矩阵,求该矩阵的k次幂,即P^k。 输入描述: 第一行:两个整数n(2<n<10)、k(1<k<5),两个数字之间用一个空格隔开,含义如上所示…...
基于Python的股票市场分析:趋势预测与策略制定
一、引言 股票市场作为投资领域的重要组成部分,其价格波动和趋势变化一直是投资者关注的焦点。准确预测股票市场的趋势对于制定有效的投资策略至关重要。本文将使用Python编程语言,结合时间序列分析和机器学习算法,对股票市场的历史数据进行…...

【C++】了解一下编码
个人主页 : zxctscl 如有转载请先通知 文章目录 1. 前言2. ASCII编码3. unicode4. GBK5. 类型转换 1. 前言 看到string里面还有Template instantiations: string其实是basic_string<char>,它还是一个模板。 再看看wstring࿱…...
生成式人工智能在金融领域:FinGPT、BloombergGPT及其未来
生成式人工智能在金融领域的应用:FinGPT、BloombergGPT 及其他 引言 生成式人工智能(Generative AI)是指能够生成与输入数据相似的新数据样本的模型。ChatGPT 的成功为各行各业带来了许多机会,激励企业设计自己的大型语言模型。…...

webpack5零基础入门-10babel的使用
Babel JavaScript 编译器。 主要用于将 ES6 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中 1.安装相关包 npm install -D babel-loader babel/core babel/preset-env 2.进行相关配置 2.1第一种写法是在webp…...

SAR ADC教程系列5——FFT频谱泄露以及相干采样
频谱泄露的出现以及如何规避? 为什么要相干采样? 1.分析ADC输出信号的频谱工具:DFT(Discrete Fourier Transform) 重点:DFT相邻频谱频率间隔为fs/N 如何规避频谱泄露? 对于DFT,它对于接收到的信…...
算法D48 | 动态规划10 | 121. 买卖股票的最佳时机 122.买卖股票的最佳时机II
股票问题是一个动态规划的系列问题,今日安排的题目不多,大家可以慢慢消化。 121. 买卖股票的最佳时机 视频讲解:https://www.bilibili.com/video/BV1Xe4y1u77q https://programmercarl.com/0121.%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A…...
Windows10安装RubyRails步骤
2024年3月14日安装,亲测。记录一下以便后续需要查看。 首先在官网下载RubyInstaller for Windows - 国内镜像 rubyinstaller.cn 版本是3.3.0 下载完后图形化界面安装 安装完毕,出现Ruby的命令行,或者在开始菜单出现start command prompt wi…...
Sqlserver 模糊查询中文及在mybatis xml【非中文不匹配查询】N@P2问题
问题 sqlserver模糊查询或相等,两者都无法查询。 百度方案解释 Like 后的N是表示unicode字符。获取SQL Server数据库中Unicode类型的数据时,字符串常量必须以大写字母 N 开头,否则字符串将转换为数据库的默认代码页(字符集编码)࿰…...

旧华硕电脑开机非常慢 电脑开机黑屏很久才显示品牌logo导致整体开机速度非常的慢怎么办
前提条件 电池需要20%(就是电池没有报废)且电脑接好电源,千万别断电,电脑会变成砖头的 解决办法 更新bios即可解决,去对应品牌官网下载最新的bios版本就行了 网上都是一些更新驱动啊...

【go语言开发】性能分析工具pprof使用
本文主要介绍如何在项目中使用pprof工具。首先简要介绍pprof工具的作用;然后介绍pprof的应用场景,主要分为工具型应用和服务型应用。最后数据分析项目,先采集项目信息,再可视化查看 文章目录 前言应用场景工具型应用服务型应用 数…...

ARM_基础之RAS
Reliability, Availability, and Serviceability (RAS), for A-profile architecture 源自 https://developer.arm.com/documentation/102105/latest/ 1 Introduction to RAS 1.1 Faults,Errors,and failures 三个概念的区分: • A failure is the event of devia…...

VScode(1)之内网离线安装开发环境(VirtualBox+ubuntu+VScode)
VScode(1)之内网离线安装开发环境(VirtualBoxubuntuVScode) Author: Once Day Date: 2022年7月18日/2024年3月17日 一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦… 漫漫长路,有人对你微笑过嘛… 全系列文…...

Python爬虫与数据可视化源码免费领取
引言 作为一名在软件技术领域深耕多年的专业人士,我不仅在软件开发和项目部署方面积累了丰富的实践经验,更以卓越的技术实力获得了🏅30项软件著作权证书的殊荣。这些成就不仅是对我的技术专长的肯定,也是对我的创新精神和专业承诺…...

Android Studio 打包 Maker MV apk 详细步骤
一.使用RPG Make MV 部署项目,获取项目文件夹 这步基本都不会有问题: 二.安装Android Studio 安装过程参考教材就行了: https://blog.csdn.net/m0_62491877/article/details/126832118 但是有的版本面板没有Android的选项(勾…...

react中hooks使用限制
只能在最顶层使用Hook 不要在循环、条件中调用hook,确保总是在React函数最顶层使用它们 只能React函数中调用Hook 不要在普通的js函数中调用 在React的函数组件中调用Hook 在自定义hook中调用其他hook 原因: 我们每次的状态值或者依赖项存在哪里&…...

2024抖音矩阵云混剪系统源码 短视频矩阵营销系统
2024抖音矩阵云混剪系统源码 短视频矩阵营销系统 矩阵营销系统多平台多账号一站式管理,一键发布作品。智能标题,关键词优化,排名查询,混剪生成原创视频,账号分组,意向客户自动采集,智能回复&am…...

力扣题目训练(22)
2024年2月15日力扣题目训练 2024年2月15日力扣题目训练563. 二叉树的坡度637. 二叉树的层平均值643. 子数组最大平均数 I304. 二维区域和检索 - 矩阵不可变154. 寻找旋转排序数组中的最小值 II 2024年2月15日力扣题目训练 2024年2月15日第二十二天编程训练,今天主要…...

网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...
设计模式和设计原则回顾
设计模式和设计原则回顾 23种设计模式是设计原则的完美体现,设计原则设计原则是设计模式的理论基石, 设计模式 在经典的设计模式分类中(如《设计模式:可复用面向对象软件的基础》一书中),总共有23种设计模式,分为三大类: 一、创建型模式(5种) 1. 单例模式(Sing…...
服务器硬防的应用场景都有哪些?
服务器硬防是指一种通过硬件设备层面的安全措施来防御服务器系统受到网络攻击的方式,避免服务器受到各种恶意攻击和网络威胁,那么,服务器硬防通常都会应用在哪些场景当中呢? 硬防服务器中一般会配备入侵检测系统和预防系统&#x…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...

AI,如何重构理解、匹配与决策?
AI 时代,我们如何理解消费? 作者|王彬 封面|Unplash 人们通过信息理解世界。 曾几何时,PC 与移动互联网重塑了人们的购物路径:信息变得唾手可得,商品决策变得高度依赖内容。 但 AI 时代的来…...
蓝桥杯 冶炼金属
原题目链接 🔧 冶炼金属转换率推测题解 📜 原题描述 小蓝有一个神奇的炉子用于将普通金属 O O O 冶炼成为一种特殊金属 X X X。这个炉子有一个属性叫转换率 V V V,是一个正整数,表示每 V V V 个普通金属 O O O 可以冶炼出 …...
CRMEB 中 PHP 短信扩展开发:涵盖一号通、阿里云、腾讯云、创蓝
目前已有一号通短信、阿里云短信、腾讯云短信扩展 扩展入口文件 文件目录 crmeb\services\sms\Sms.php 默认驱动类型为:一号通 namespace crmeb\services\sms;use crmeb\basic\BaseManager; use crmeb\services\AccessTokenServeService; use crmeb\services\sms\…...
Linux系统部署KES
1、安装准备 1.版本说明V008R006C009B0014 V008:是version产品的大版本。 R006:是release产品特性版本。 C009:是通用版 B0014:是build开发过程中的构建版本2.硬件要求 #安全版和企业版 内存:1GB 以上 硬盘…...
libfmt: 现代C++的格式化工具库介绍与酷炫功能
libfmt: 现代C的格式化工具库介绍与酷炫功能 libfmt 是一个开源的C格式化库,提供了高效、安全的文本格式化功能,是C20中引入的std::format的基础实现。它比传统的printf和iostream更安全、更灵活、性能更好。 基本介绍 主要特点 类型安全:…...

【深度学习新浪潮】什么是credit assignment problem?
Credit Assignment Problem(信用分配问题) 是机器学习,尤其是强化学习(RL)中的核心挑战之一,指的是如何将最终的奖励或惩罚准确地分配给导致该结果的各个中间动作或决策。在序列决策任务中,智能体执行一系列动作后获得一个最终奖励,但每个动作对最终结果的贡献程度往往…...