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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/752116.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

git问题列表(一)(持续更新中~~~)

文章目录 问题1&#xff1a;如何在本地创建git仓库&#xff0c;并推送到远程仓库&#xff1f;问题2&#xff1a;如何创建本地分支&#xff0c;并基于其创建远程分支&#xff1f;问题3&#xff1a;报错“origin does not appear to be a git repository”是什么原因&#xff1f;…

如何在Ubuntu中查看编辑lvgl的demo和examples?

如何在Ubuntu中查看编辑lvgl的demo和examples&#xff1f; 如何在 Ubuntu系统中运行查看lvgl 1、拉取代码 在lvgl的github主页面有50多个仓库&#xff0c;找到lv_port_pc_eclipse这个仓库&#xff0c;点进去 拉取仓库代码和子仓库代码 仓库网址&#xff1a;https://github…

【php基础】输出、变量、

php基础补充 1. 输出2.和"的区别3.变量3.1变量的命名规则3.2 两个对象指向同一个值3.3 可变变量 4.变量的作用域5. 检测变量 1. 输出 echo: 输出 print: 输出&#xff0c;输出成功返回1 print_r(): 输出数组 var_dump(): 输出数据的详细信息&#xff0c;带有数据类型和数…

矩阵中移动的最大次数

文章目录 所属专栏:BFS算法 题目链接 思路如下&#xff1a; 1.首先我们需要从第一列开始遍历&#xff0c;寻找每一个都能够满足条件的位置&#xff0c;将它插入到数组里面 2.第一列遍历完了后我们先判断第一列的数是否都满足条件插入到数组里面&#xff0c;如果数组为空&#…

智障版本GPT3实现

背景,实现GPT3,采用python代码。调库hf及tf2.0+基础。 由于完全实现GPT模型及其预训练过程涉及大量的代码和计算资源,以下是一个基于TensorFlow 2.x的简化版GPT模型构建和调用的示例。请注意,这仅展示了模型的基本结构,实际运行需替换为真实数据集和预处理步骤,且无法直…

深入理解Transformer架构:从Seq2Seq到无监督预训练的演进

Transformer问答-2 根据我的了解&#xff0c;最开始Transformer的架构为encoderdecoder&#xff0c;是用于处理Seq2Seq任务的&#xff0c;后来GPT系列只采用decoder-only架构专注于next toke prediction任务&#xff0c;在我的认识当中&#xff0c;这两者都可以归为next toke …

基于粒子群算法的分布式电源配电网重构优化matlab仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 4.1基本PSO算法原理 4.2配电网重构的目标函数 5.完整工程文件 1.课题概述 基于粒子群算法的分布式电源配电网重构优化。通过Matlab仿真&#xff0c;对比优化前后 1.节点的电压值 2.线路的损耗,这里计…

18双体系Java学习之数组赋值和拷贝

数组赋值 数组拷贝 ★小贴士 Object src指定源数组&#xff0c; int srcPos指定复制开始的位置&#xff0c; Object dest指目标数组&#xff0c; int destPos指定复制的内容从哪个位置开始放置&#xff0c; int length 指复制的长度&#xff0c; 也就是说源数组中位置从 s…

ubuntu18.04安装ffmpeg

编译ffmpeg命令如下&#xff1a; 1.安装yasm wget http://www.tortall.net/projects/yasm/releases/yasm-1.3.0.tar.gz tar xvzf yasm-1.3.0.tar.gz cd yasm-1.3.0 ./configure make && make install2.安装nasm(2.13以上版本) wget https://www.nasm.us/pub/nasm/re…

如何解决Hexo个人博客上传GitHub后无法更新问题

HexoGitHub提供了一个轻量级、易于管理的平台&#xff0c;让大家可以轻松建立并维护自己的博客。然而&#xff0c;许多初次使用Hexo的朋友可能会遇到一个常见问题&#xff1a;在将博客上传到GitHub之后&#xff0c;对博客进行更新&#xff0c;但是网页上的内容却没有改变。本文…

linux环境下连接db2数据库的操作

linux环境下连接db2数据库的操作 1 查看db2连接相关信息2 建立连接3 取消连接4 测试连接 1 查看db2连接相关信息 su - db2inst1 ~]$ db2 list db directory ~]$ db2 list node directory ~]$ db2 list dcs directory2 建立连接 su - db2inst1 ~]$ db2 terminate ~]$ db2 cata…

数据结构大合集02——线性表的相关函数运算算法

函数运算算法合集02 顺序表的结构体顺序表的基本运算的实现1. 建立顺序表2. 顺序表的基本运算2.1 初始化线性表2. 2 销毁顺序表2.3 判断顺序表是否为空表2.4 求顺序表的长度2.5 输出顺序表2.6 按序号求顺序表中的元素2.7 按元素值查找2.8 插入数据元素2.9 删除数据元素 单链表的…

如何使用人工智能打造超用户预期的个性化购物体验

回看我的营销职业生涯&#xff0c;我见证了数字时代如何重塑客户期望。从一刀切的方法过渡到创造高度个性化的购物体验已成为企业的关键。在这个客户期望不断变化的新时代&#xff0c;创造个性化的购物体验不再是奢侈品&#xff0c;而是企业的必需品。人工智能 &#xff08;AI&…

插件电阻的工作原理,结构特点,工艺流程,选型参数及设计注意事项总结

🏡《总目录》 目录 1,概述2,工作原理3,结构特点3.1,引脚设计3.2,电阻体3.3,封装4,工艺流程4.1,材料准备4.2,电阻体制作4.3,引脚焊接4.4,绝缘处理4.5,测试与筛选4.6,包装与存储

常见的十大网络安全攻击类型

常见的十大网络安全攻击类型 网络攻击是一种针对我们日常使用的计算机或信息系统的行为&#xff0c;其目的是篡改、破坏我们的数据&#xff0c;甚至直接窃取&#xff0c;或者利用我们的网络进行不法行为。你可能已经注意到&#xff0c;随着我们生活中越来越多的业务进行数字化&…

[数据集][目标检测]焊接件表面缺陷检测数据集VOC+YOLO格式2292张10类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;2292 标注数量(xml文件个数)&#xff1a;2292 标注数量(txt文件个数)&#xff1a;2292 标注…

阿里云下载安装centos

这里以centos7.x版本下载安装为例 : 网址 : 阿里巴巴开源镜像站-OPSX镜像站-阿里云开发者社区 点击centos : 再点击下载地址 : 找到 7/ 并点击 : 找到isos/并点击 : 点击x86_64 : 找到4.4G的文件点击下载 ; 点击创建新的虚拟机 , 然后选择典型 &#xff0c; 然后点击下一…

栈和队列(Java实现)

栈和队列&#xff08;Java实现&#xff09; 栈 栈(Stack)&#xff1a;栈是先进后出&#xff08;FILO, First In Last Out&#xff09;的数据结构。Java中实现栈有以下两种方式&#xff1a; stack类LinkedList实现&#xff08;继承了Deque接口&#xff09; &#xff08;1&am…

Docker入门一(Docker介绍、Docker整体结构、Docker安装、镜像、容器、Docker的容器与镜像)

文章目录 一、Docker介绍1.什么是虚拟化2.虚拟化模块3.docker是什么4.docker平台介绍5.为什么使用docker6.docker主要解决的问题 二、docker整体结构1.Docker引擎介绍&#xff08;Docker Engine&#xff09;2.Docker结构概览介绍3.Docker底层技术 三、docker安装1.Docker-CE和D…

Python实现连连看

# coding:utf-8 import pygame, sys, random, time, easygui from pygame.locals import * # 初始化pygame环境 pygame.init() # 创建窗口 canvas pygame.display.set_mode((1000, 600)) # 加载图片 bg pygame.image.load("imgs/bg.jpg") win pygame.image.load(&…