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日第二十二天编程训练,今天主要…...

龙虎榜——20250610
上证指数放量收阴线,个股多数下跌,盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型,指数短线有调整的需求,大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的:御银股份、雄帝科技 驱动…...
反向工程与模型迁移:打造未来商品详情API的可持续创新体系
在电商行业蓬勃发展的当下,商品详情API作为连接电商平台与开发者、商家及用户的关键纽带,其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息(如名称、价格、库存等)的获取与展示,已难以满足市场对个性化、智能…...

Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...
蓝桥杯 2024 15届国赛 A组 儿童节快乐
P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡,轻快的音乐在耳边持续回荡,小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下,六一来了。 今天是六一儿童节,小蓝老师为了让大家在节…...
【android bluetooth 框架分析 04】【bt-framework 层详解 1】【BluetoothProperties介绍】
1. BluetoothProperties介绍 libsysprop/srcs/android/sysprop/BluetoothProperties.sysprop BluetoothProperties.sysprop 是 Android AOSP 中的一种 系统属性定义文件(System Property Definition File),用于声明和管理 Bluetooth 模块相…...
Robots.txt 文件
什么是robots.txt? robots.txt 是一个位于网站根目录下的文本文件(如:https://example.com/robots.txt),它用于指导网络爬虫(如搜索引擎的蜘蛛程序)如何抓取该网站的内容。这个文件遵循 Robots…...
实现弹窗随键盘上移居中
实现弹窗随键盘上移的核心思路 在Android中,可以通过监听键盘的显示和隐藏事件,动态调整弹窗的位置。关键点在于获取键盘高度,并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...
使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度
文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...

HashMap中的put方法执行流程(流程图)
1 put操作整体流程 HashMap 的 put 操作是其最核心的功能之一。在 JDK 1.8 及以后版本中,其主要逻辑封装在 putVal 这个内部方法中。整个过程大致如下: 初始判断与哈希计算: 首先,putVal 方法会检查当前的 table(也就…...
站群服务器的应用场景都有哪些?
站群服务器主要是为了多个网站的托管和管理所设计的,可以通过集中管理和高效资源的分配,来支持多个独立的网站同时运行,让每一个网站都可以分配到独立的IP地址,避免出现IP关联的风险,用户还可以通过控制面板进行管理功…...