threejs案例,与静态三角形网格的基本碰撞, 鼠标环顾四周并投球游戏

  1. 创建一个时钟对象:
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.11000是相机的近裁剪面和远裁剪面。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 );

这部分代码添加了一个性能监控器,用于显示渲染器的帧率和其他相关信息。

  1. 常量定义:
const GRAVITY = 30;
const NUM_SPHERES = 100;
const SPHERE_RADIUS = 0.2;
const STEPS_PER_FRAME = 5;

这里定义了一些常量,用于控制球体的数量、半径、重力加速度和每帧的模拟步骤数。

  1. 创建球体几何体和材质:
const sphereGeometry = new THREE.IcosahedronGeometry( SPHERE_RADIUS, 5 );
const sphereMaterial = new THREE.MeshLambertMaterial( { color: 0xdede8d } );

使用THREE.IcosahedronGeometry创建了一个二十面体几何体,用于作为球体的基础形状。然后,定义了一个Lambert材质,这种材质适用于模拟非直接光照的情况,并设置了球体的颜色。

  1. 创建和添加球体到场景中:
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的球体),用于物理模拟,以及一个速度向量。

  1. 创建八叉树:
const worldOctree = new Octree();

八叉树(Octree)是一种用于3D空间分割的数据结构,常用于碰撞检测和空间索引。这里创建了一个新的八叉树实例,但代码中没有显示其如何使用。

  1. 创建玩家碰撞体和速度向量:
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库的一部分。

  1. 初始化其他变量:
let playerOnFloor = false;
let mouseTime = 0;
const keyStates = {};
const vector1 = new THREE.Vector3();
const vector2 = new THREE.Vector3();
const vector3 = new THREE.Vector3();

这些变量可能用于控制玩家的状态(如是否在地板上)、鼠标交互、键盘输入以及临时存储向量计算的结果。

  1. 键盘按键监听:
document.addEventListener( 'keydown', ( event ) => {
    keyStates[ event.code ] = true;
} );

document.addEventListener( 'keyup', ( event ) => {
    keyStates[ event.code ] = false;
} );

这里为文档对象(整个页面)添加了两个事件监听器,分别用于处理键盘的keydownkeyup事件。keydown事件在用户按下键时触发,keyup则在键被释放时触发。event.code提供了被按下或释放的键的标识符。keyStates是一个对象,用于存储每个键的当前状态(按下或释放)。

  1. 鼠标按下监听:
container.addEventListener( 'mousedown', () => {
    document.body.requestPointerLock();
    mouseTime = performance.now();
} );

当在container元素上按下鼠标时,这段代码请求将指针锁定到页面上,这样当鼠标移动时,鼠标指针将不再显示,而页面的其他部分也不会接收鼠标事件。mouseTime变量存储了鼠标按下的时间,可能是为了计算后续鼠标移动的持续时间或速度。

  1. 鼠标释放监听:
document.addEventListener( 'mouseup', () => {
    if ( document.pointerLockElement !== null ) throwBall();
} );

当鼠标按钮释放时,如果指针已经被锁定(即document.pointerLockElement不为null),则调用throwBall函数。从这段代码看不出throwBall函数的实现细节,但可以猜测它的作用是抛出某种对象(可能是一个球体)。

  1. 鼠标移动监听:
document.body.addEventListener( 'mousemove', ( event ) => {
    if ( document.pointerLockElement === document.body ) {
        camera.rotation.y -= event.movementX / 500;
        camera.rotation.x -= event.movementY / 500;
    }
} );

当鼠标在文档体上移动时,如果指针被锁定到文档体上,这段代码会更新相机的旋转。event.movementXevent.movementY分别表示鼠标在X和Y轴上的移动量。通过除以500,代码将鼠标的移动量转换为较小的相机旋转量,从而实现平滑的相机控制。

  1. 窗口大小调整监听:
window.addEventListener( 'resize', onWindowResize );

function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize( window.innerWidth, window.innerHeight );
}

这段代码监听窗口的大小调整事件。当窗口大小变化时,它会调用onWindowResize函数,该函数会更新相机的纵横比,并更新其投影矩阵。此外,它还调用renderer.setSize来确保渲染器的大小与窗口的大小相匹配。

这段代码是用于处理三维场景中的球体和玩家(或相机)的交互和移动。具体来说,它定义了三个函数:throwBallplayerCollisionsupdatePlayer。以下是对这些函数的详细解析:

throwBall 函数

这个函数用于投掷一个球体(可能是一个游戏对象,如球)。

  1. 获取球体:

    const sphere = spheres[ sphereIdx ];
    

    spheres数组中获取一个球体,sphereIdx是当前的球体索引。

  2. 设置投掷方向:

    camera.getWorldDirection( playerDirection );
    sphere.collider.center.copy( playerCollider.end ).addScaledVector( playerDirection, playerCollider.radius * 1.5 );
    

    首先获取相机的世界方向(玩家面向的方向),然后将球体的位置设置为玩家碰撞体的末端,并沿着玩家方向移动一定距离(玩家碰撞体半径的1.5倍)。

  3. 计算投掷力度:

    const impulse = 15 + 30 * ( 1 - Math.exp( ( mouseTime - performance.now() ) * 0.001 ) );
    

    这里计算投掷的力度(或冲量)。力度基于鼠标按下的时间(mouseTime)和当前时间(performance.now())的差值来计算。时间差越大,力度越大。

  4. 设置球体的速度和方向:

    sphere.velocity.copy( playerDirection ).multiplyScalar( impulse );
    sphere.velocity.addScaledVector( playerVelocity, 2 );
    

    球体的速度被设置为玩家方向,并乘以计算出的力度。然后,再基于玩家的速度进行微调。

  5. 更新球体索引:

    sphereIdx = ( sphereIdx + 1 ) % spheres.length;
    

    更新球体索引,以便下一次投掷时可以使用下一个球体。

playerCollisions 函数

这个函数处理玩家(或相机的碰撞体)与场景中的其他物体之间的碰撞。

  1. 检测碰撞:

    const result = worldOctree.capsuleIntersect( playerCollider );
    

    使用worldOctree(可能是一个空间划分的数据结构,用于加速碰撞检测)检测玩家的碰撞体是否与任何物体相交。

  2. 判断是否在地面上:

    playerOnFloor = false;
    if ( result ) {
        playerOnFloor = result.normal.y > 0;
        // ...
    }
    

    如果发生碰撞,检查碰撞的法线(result.normal)的y分量是否大于0来判断玩家是否在地面上。

  3. 处理非地面碰撞:

    if ( ! playerOnFloor ) {
        playerVelocity.addScaledVector( result.normal, - result.normal.dot( playerVelocity) );
    }
    

    如果玩家不在地面上,更新玩家的速度以反映碰撞的影响。

  4. 移动碰撞体以处理穿透:

    playerCollider.translate( result.normal.multiplyScalar( result.depth) );
    

    移动玩家的碰撞体以处理任何可能发生的穿透。

updatePlayer 函数

这个函数用于更新玩家的位置和速度。

  1. 计算阻尼:

    let damping = Math.exp( - 4 * deltaTime ) - 1;
    

    阻尼用于模拟玩家速度的逐渐减小(例如空气阻力或摩擦)。

  2. 处理玩家在空中的移动:

    if ( ! playerOnFloor ) {
        playerVelocity.y -= GRAVITY * deltaTime;
        damping *= 0.1;
    }
    

    如果玩家不在地面上,更新玩家的y速度以模拟重力,并减小阻尼。

  3. 更新玩家速度:

    playerVelocity.addScaledVector( playerVelocity, damping );
    

    根据阻尼更新玩家的速度。

  4. 移动玩家碰撞体:

    const deltaPosition = playerVelocity.clone().multiplyScalar( deltaTime );
    playerCollider.translate( deltaPosition );
    

    计算玩家应该移动的距离,并更新玩家的碰撞体位置。

  5. 检测碰撞并更新相机位置:

    playerCollisions();
    camera.position.copy( playerCollider.end );
    

    调用playerCollisions函数来处理可能的碰撞,然后将相机的位置设置为玩家碰撞体的末端。

playerSphereCollision 函数

这个函数处理玩家球体与另一个球体的碰撞。

  1. 计算玩家碰撞体的中心:

    const center = vector1.addVectors( playerCollider.start, playerCollider.end ).multiplyScalar( 0.5 );
    

    玩家碰撞体可能是一个胶囊体(capsule),这里计算其中心点。

  2. 获取球体的中心:

    const sphere_center = sphere.collider.center;
    
  3. 计算碰撞半径:

    const r = playerCollider.radius + sphere.collider.radius;
    const r2 = r * r;
    

    这是玩家碰撞体和球体碰撞体半径之和,以及它的平方。

  4. 碰撞检测:

    for ( const point of [ playerCollider.start, playerCollider.end, center ] ) {
        // ...
    }
    

    这里将玩家碰撞体近似为三个点(开始点、结束点和中心点)进行碰撞检测。对于每个点,它计算到球体中心的距离平方,并检查这个距离是否小于两个碰撞体半径之和的平方。

  5. 碰撞响应:
    如果发生碰撞,代码计算碰撞法线(normal),然后根据法线和两个物体的速度来计算碰撞后的新速度。同时,调整球体的位置以解决任何可能的穿透。

spheresCollisions 函数

这个函数处理球体之间的碰撞。

  1. 双层循环遍历球体:

    for ( let i = 0, length = spheres.length; i < length; i ++ ) {
        // ...
        for ( let j = i + 1; j < length; j ++ ) {
            // ...
        }
    }
    

    使用两个嵌套的循环来遍历所有球体对。这样可以确保每个球体对只被检查一次。

  2. 计算球体间的距离平方:

    const d2 = s1.collider.center.distanceToSquared( s2.collider.center );
    
  3. 检查碰撞:
    如果两个球体中心之间的距离小于它们半径之和,则发生碰撞。

  4. 碰撞响应:
    类似于playerSphereCollision函数,根据碰撞法线和两个球体的速度来计算新的速度,并调整球体的位置。

这段代码实现了三维空间中的碰撞检测和响应。它使用了简化的碰撞检测(将玩家碰撞体近似为三个点)和基于物理的碰撞响应(改变物体的速度和位置)。这种碰撞处理在实时渲染和物理模拟中非常常见,特别是在游戏开发中。

这段代码主要实现了三维空间中多个球体的更新和碰撞处理。以下是对代码的详细解析:

updateSpheres 函数

这个函数负责更新所有球体的位置和速度,并处理它们与场景中其他物体的碰撞。

  1. 更新球体位置:

    sphere.collider.center.addScaledVector( sphere.velocity, deltaTime );
    

    根据球体的速度和经过的时间(deltaTime)来更新球体的位置。

  2. 检查球体与场景中的碰撞:

    const result = worldOctree.sphereIntersect( sphere.collider );
    

    使用worldOctree(可能是一个用于空间划分的数据结构)来检查球体是否与场景中的其他物体发生碰撞。

  3. 处理碰撞:
    如果发生碰撞(result为真),则调整球体的速度和位置以响应碰撞。这里使用了基于物理的碰撞响应,通过沿着碰撞法线(result.normal)反向推动球体来解决穿透问题。

  4. 处理重力:
    如果球体没有碰撞到任何东西(result为假),则球体的y轴速度(竖直方向)会受到重力的影响而减小。

  5. 速度阻尼:

    const damping = Math.exp( - 1.5 * deltaTime ) - 1;
    sphere.velocity.addScaledVector( sphere.velocity, damping );
    

    对球体的速度应用阻尼,使其逐渐减小。这可以模拟空气阻力或其他形式的能量损失。

  6. 检查与玩家的碰撞:

    playerSphereCollision( sphere );
    

    调用playerSphereCollision函数来检查球体是否与玩家发生碰撞,并相应地更新它们的速度和位置。

  7. 处理球体间的碰撞:

    spheresCollisions();
    

    调用spheresCollisions函数来处理球体之间的碰撞。

  8. 更新球体的视觉表示:

    sphere.mesh.position.copy( sphere.collider.center );
    

    将球体的视觉表示(mesh)的位置更新为其碰撞体的中心位置,以确保视觉和物理状态一致。

getForwardVector 函数

这个函数返回相机(或玩家)的前方向量。

  1. 获取相机的世界方向:

    camera.getWorldDirection( playerDirection );
    

    获取相机在世界坐标系中的方向。

  2. 调整方向并归一化:

    playerDirection.y = 0;
    playerDirection.normalize();
    

    将方向向量的y分量设为0(即忽略竖直方向),然后归一化向量。

getSideVector 函数

这个函数返回相机(或玩家)的侧向量。

  1. 获取相机的世界方向:

    camera.getWorldDirection( playerDirection );
    

    同样获取相机在世界坐标系中的方向。

  2. 计算侧向量:

    playerDirection.cross( camera.up );
    

    通过计算相机方向向量与上方向向量的叉积来得到侧向量。

  3. 玩家控制(controls 函数):

    • 根据deltaTime(上一次渲染到当前渲染的时间差)和玩家是否在地面(playerOnFloor)来设定玩家的移动速度(speedDelta)。
    • 根据按键状态(keyStates)来更新玩家的速度(playerVelocity)。具体来说,如果按下’W’键,玩家会向前移动;如果按下’S’键,玩家会向后移动;如果按下’A’键,玩家会向左移动;如果按下’D’键,玩家会向右移动。
    • 如果玩家在地面并且按下空格键,玩家的垂直速度(y轴)会设置为15,实现跳跃功能。
  4. 模型加载和场景设置:

    • 使用GLTFLoader来加载一个名为collision-world.glb的3D模型。
    • 将加载的模型(gltf.scene)添加到场景(scene)中。
    • 创建一个worldOctree数据结构来存储场景的碰撞信息,并通过gltf.scene来初始化它。
    • 遍历模型的每一个子节点,如果它是网格(isMesh),则设置它的阴影投射和接收属性,并优化其贴图的采样(通过设置anisotropy)。
    • 创建一个OctreeHelper对象,这是一个用于可视化worldOctree的辅助对象,但默认是隐藏的。
    • 使用GUI库来创建一个界面元素,用户可以通过这个界面来切换OctreeHelper的可见性。
  5. 玩家位置重置(teleportPlayerIfOob 函数):

    • 如果相机(代表玩家)的位置在y轴上的值小于或等于-25(可能表示玩家掉出了世界边界),则将玩家的碰撞体(playerCollider)的位置和大小重置,并将相机位置设置为新的碰撞体位置,同时将相机的旋转重置。这实际上是将玩家“传送”回世界中的某个安全位置。

整体上,这段代码为3D场景中的玩家控制、模型加载和场景设置以及玩家位置重置提供了实现。其中,controls函数负责根据玩家的输入更新玩家的速度,而模型加载和场景设置部分则通过加载一个3D模型并设置其相关属性来初始化场景。最后,teleportPlayerIfOob函数提供了一个机制来确保玩家不会掉出世界的边界。

animate 函数是3D渲染应用中常见的动画循环函数,用于在每个动画帧中更新场景的状态并渲染场景。以下是对这段代码的详细解析:

代码功能概述

  1. 计算时间差(deltaTime: 通过 clock.getDelta() 获取从上一次动画帧到现在的时间差(以秒为单位),并限制其最大值为0.05秒。之后,将这个时间差除以 STEPS_PER_FRAME,得到一个子步长的时间差。

  2. 碰撞检测的子步处理: 通过一个循环,执行 STEPS_PER_FRAME 次更新操作,每次循环中都会调用玩家控制和更新函数,以此来降低物体快速移动导致碰撞检测失败的风险。

  3. 更新函数:

    • controls( deltaTime ): 根据当前的时间差 deltaTime 更新玩家的速度和位置。
    • updatePlayer( deltaTime ): 更新玩家的状态或位置,可能还包括与环境的交互。
    • updateSpheres( deltaTime ): 更新场景中的球体(或其他物体)的状态。
    • teleportPlayerIfOob(): 如果玩家超出边界,则将其传送回安全位置。
  4. 渲染场景: 使用 renderer.render( scene, camera ) 渲染当前的场景和相机视图。

  5. 更新性能统计: stats.update() 可能用于更新一些性能统计信息,比如帧率等。

  6. 请求下一帧: 使用 requestAnimationFrame( animate ) 请求浏览器的下一帧动画,并将 animate 函数作为回调函数,从而形成一个连续的动画循环。

细节分析

  • deltaTime 的计算考虑了 STEPS_PER_FRAME,这通常用于减少快速移动物体错过碰撞检测的机会。通过将总的 deltaTime 分割成多个子步,每个子步中更新物体的位置,可以提高碰撞检测的准确性。

  • controls 函数负责根据用户的输入和当前的时间差来更新玩家的移动状态。

  • updatePlayerupdateSpheres 函数分别更新玩家和场景中球体的状态。这些更新可能包括位置、速度、动画状态等。

  • 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 forward

				const 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 resistance
					damping *= 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 spheres

				for ( 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 control
				const 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>

本内容来源于小豆包,想要更多内容请跳转小豆包 》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小豆包3D世界

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值