首页IT科技乒乓球3d虚拟世界之旅(Three.js 进阶之旅:物理效果-3D乒乓球小游戏 🏓)

乒乓球3d虚拟世界之旅(Three.js 进阶之旅:物理效果-3D乒乓球小游戏 🏓)

时间2025-06-20 00:54:01分类IT科技浏览6198
导读:声明:本文涉及图文和模型素材仅用于个人学习、研究和欣赏,请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。...

声明:本文涉及图文和模型素材仅用于个人学习                、研究和欣赏               ,请勿二次修改                       、非法传播       、转载                、出版                        、商用       、及进行其他获利行为               。

摘要

本文在专栏上一篇内容《Three.js 进阶之旅:物理效果-碰撞和声音》的基础上                       ,将使用新的技术栈 React Three Fiber 和 Cannon.js 来实现一个具有物理特性的小游戏        ,通过本文的阅读        ,你将学习到的知识点包括:了解什么是 React Three Fiber 及它的相关生态        、使用 React Three Fiber 搭建基础三维场景                        、如何使用新技术栈给场景中对象的添加物理特性等                       ,最后利用上述知识点                ,将开发一个简单的乒乓球小游戏                       。

效果

在正式学习之前        ,我们先来看看本文示例最终实现效果:页面主体内容是一个手握乒乓球拍的模型和一个乒乓球 🏓                       ,对球拍像现实生活中一样进行颠球施力操作                ,乒乓球可以在球拍上弹起,乒乓球弹起的高度随着施加在球拍上的力的大小的变化而变化                       ,球拍中央显示的是连续颠球次数 5️⃣                        ,当乒乓球从球拍掉落时一局游戏结束,球拍上的数字归零 0️⃣         。快来试试你一次可以颠多少个球吧 😏        。

打开以下链接               ,在线预览效果                        ,大屏访问效果更佳                       。

👁‍🗨 在线预览地址:https://dragonir.github.io/physics-pingpong/

本专栏系列代码托管在 Github 仓库【threejs-odessey】        ,后续所有目录也都将在此仓库中更新                。

🔗 代码仓库地址:git@github.com:dragonir/threejs-odessey.git

原理

React-Three-Fiber

React Three Fiber 是一个基于 Three.js 的 React 渲染器               ,简称 R3F        。它像是一个配置器                       ,把 Three.js 的对象映射为 R3F 中的组件                       。以下是一些相关链接:

仓库: https://github.com/pmndrs/react-three-fiber 官网: https://docs.pmnd.rs/react-three-fiber/getting-started/introduction 示例: https://docs.pmnd.rs/react-three-fiber/getting-started/examples 特点 使用可重用的组件以声明方式构建动态场景图        ,使 Three.js 的处理变得更加轻松        ,并使代码库更加整洁                。这些组件对状态变化做出反应                       ,具有开箱即用的交互性。 Three.js 中所有内容都能在这里运行                       。它不针对特定的 Three.js 版本                ,也不需要更新以修改        ,添加或删除上游功能                        。 渲染性能与 Three.js 和 GPU 相仿。组件参与 React 之外的 render loop 时                       ,没有任何额外开销               。

写 React Three Fiber 比较繁琐                ,我们可以写成 R3F 或简称为 Fiber                        。让我们从现在开始使用 R3F 吧        。

生态系统

R3F 有充满活力的生态系统,包括各种库               、辅助工具以及抽象方法:

@react-three/drei – 有用的辅助工具                       ,自身就有丰富的生态 @react-three/gltfjsx – 将 GLTFs 转换为 JSX 组件 @react-three/postprocessing – 后期处理效果 @react-three/test-renderer – 用于在 Node 中进行单元测试 @react-three/flex – react-three-fiber 的 flex 盒子布局 @react-three/xr – VR/AR 控制器和事件 @react-three/csg – 构造实体几何 @react-three/rapier – 使用 Rapier 的 3D 物理引擎 @react-three/cannon – 使用 Cannon 的 3D 物理引擎 @react-three/p2 – 使用 P2 的 2D 物理引擎 @react-three/a11y – 可访问工具 @react-three/gpu-pathtracer – 真实的路径追踪 create-r3f-app next – nextjs 启动器 lamina – 基于 shader materials 的图层 zustand – 基于 flux 的状态管理 jotai – 基于 atoms 的状态管理 valtio – 基于 proxy 的状态管理 react-spring – 一个 spring-physics-based 的动画库 framer-motion-3d – framer motion                        ,一个很受欢迎的动画库 use-gesture – 鼠标/触摸手势 leva – 创建 GUI 控制器 maath – 数学辅助工具 miniplex – ECS 实体管理系统 composer-suite – 合成着色器        、粒子                        、特效和游戏机制               、 安装 npm install three @react-three/fiber 第一个场景

在一个新建的 React 项目中,我们通过以下的步骤使用 R3F 来创建第一个场景               。

初始化Canvas

首先               ,我们从 @react-three/fiber 引入 Canvas 元素                        ,将其放到 React 树中:

import ReactDOM from react-dom import { Canvas } from @react-three/fiber function App() { return ( <div id="canvas-container"> <Canvas /> </div> ) } ReactDOM.render(<App />, document.getElementById(root))

Canvas 组件在幕后做了一些重要的初始化工作:

它初始化了一个场景 Scene 和一个相机 Camera        ,它们都是渲染所需的基本模块                       。 它在页面每一帧更新中都渲染场景               ,我们不需要再到页面重绘方法中循环调用渲染方法        。

🚩 Canvas 大小响应式自适应于父节点                       ,我们可以通过改变父节点的宽度和高度来控制渲染场景的尺寸大小        。

添加一个Mesh组件

为了真正能够在场景中看到一些物体        ,现在我们添加一个小写的 <mesh /> 元素        ,它直接等效于 new THREE.Mesh()                       。

<Canvas> <mesh />

🚩 可以看到我们没有特地去额外引入mesh组件                       ,我们不需要引入任何元素                ,所有Three.js中的对象都将被当作原生的JSX元素        ,就像在 ReactDom 中写 <div /> 及 <span /> 元素一样                。R3F Fiber组件的通用规则是将Three.js中的它们的名字写成驼峰式的DOM元素即可        。

一个 Mesh 是 Three.js 中的基础场景对象                       ,需要给它提供一个几何对象 geometry 以及一个材质 material 来代表一个三维空间的几何形状                ,我们将使用一个 BoxGeometry 和 MeshStandardMaterial 来创建一个新的网格 Mesh,它们会自动关联到它们的父节点                       。

<Canvas> <mesh> <boxGeometry /> <meshStandardMaterial /> </mesh>

上述代码和以下 Three.js 代码是等价的:

const scene = new THREE.Scene() const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000) const renderer = new THREE.WebGLRenderer() renderer.setSize(width, height) document.querySelector(#canvas-container).appendChild(renderer.domElement) const mesh = new THREE.Mesh() mesh.geometry = new THREE.BoxGeometry() mesh.material = new THREE.MeshStandardMaterial() scene.add(mesh) function animate() { requestAnimationFrame(animate) renderer.render(scene, camera) } animate()

构造函数参数

根据 BoxGeometry 的文档                       ,我们可以选择给它传递三个参数:width、length 及 depth:

new THREE.BoxGeometry(2, 2, 2)

为了实现相同的功能                        ,我们可以在 R3F 中使用 args 属性,它总是接受一个数组               ,其项目表示构造函数参数:

<boxGeometry args={[2, 2, 2]} /> 添加光源

接着                        ,我们通过像下面这样添加光源组件来为我们的场景添加一些光线                。

<Canvas> <ambientLight intensity={0.1} /> <directionalLight position={[0, 0, 5]} />

属性

这里介绍关于 R3F 的最后一个概念        ,即 React 属性是如何在 Three.js 对象中工作的。当你给一个 Fiber 组件设置任意属性时               ,它将对 Three.js 设置一个相同名字的属性                       。我们关注到 ambientLight 上                       ,由它的文档可知        ,我们可以选择 color 和 intensity 属性来初始化它:

<ambientLight intensity={0.1} />

等价于

const light = new THREE.AmbientLight() light.intensity = 0.1

快捷方法

在 Three.js 中对于很多属性的设置如 colors                        、vectors 等都可以使用 set() 方法进行快捷设置:

const light = new THREE.DirectionalLight() light.position.set(0, 0, 5) light.color.set(red)

在 JSX 中也是相同的:

<directionalLight position={[0, 0, 5]} /> 结果 <Canvas> <mesh> <boxBufferGeometry /> <meshBasicMaterial /> </mesh> <ambientLight args={[0xff0000]} intensity={0.1} /> <directionalLight position={[0, 0, 5]} intensity={0.5} /> </Canvas>

查看React Three Fiber完整API文档

实现

到这里        ,我们已经掌握了 R3F 的基本知识                       ,我们再结合专栏上篇关于物理特性的内容                ,来实现如文章开头介绍的乒乓球 🏓 小游戏                        。

🚩 本文乒乓球小游戏基础版及乒乓球三维模型资源来源于R3F官网示例。

〇 搭建页面基本结构

首先        ,我们创建一个 Experience 文件作为渲染三维场景的组件                       ,并在其中添加 Canvas 组件搭建基本页面结构               。

import { Canvas } from "@react-three/fiber"; export default function Experience() { return ( <> <Canvas></Canvas> </> ); }

① 场景初始化

接着我们开启 Canvas 的阴影并设置相机参数                ,然后添加环境光 ambientLight 和点光源 pointLight 两种光源:

<Canvas shadows camera={{ fov: 50, position: [0, 5, 12] }} > <ambientLight intensity={.5} /> <pointLight position={[-10, -10, -10]} /> </Canvas>

如果需要修改 Canvas 的背景色,可以在其中添加一个 color 标签并设置参数 attach 为 background                       ,在 args 参数中设置颜色即可                        。

<Canvas> <color attach="background" args={["lightgreen"]} /> </Canvas>

② 添加辅助工具

接着                        ,我们在页面顶部引入 Perf,它是 R3F 生态中查看页面性能的组件               ,它的功能和 Three.js 中 stats.js 是类似的                        ,像下面这样添加到代码中设置它的显示位置        ,页面对应区域就会出现可视化的查看工具               ,在上面可以查看 GPU                       、CPU、FPS 等性能参数        。

如果想使用网格作为辅助线或用作装饰                       ,可以使用 gridHelper 组件        ,它支持配置 position                、rotation                       、args 等参数               。

import { Perf } from "r3f-perf"; export default function Experience() { return ( <> <Canvas> <Perf position="top-right" /> <gridHelper args={[50, 50, #11f1ff, #0b50aa]} position={[0, -1.1, -4]} rotation={[Math.PI / 2.68, 0, 0]} /> </Canvas> </> ); }

③ 创建乒乓球和球拍

我们创建一个名为 PingPong.jsx 的乒乓球组件文件        ,然后在文件顶部引入以下依赖                       ,其中 Physics       、useBox                、usePlane                        、useSphere 用于创建物理世界;useFrame 是用来进行页面动画更新的 hook                ,它将在页面每帧重绘时执行        ,我们可以在它里面执行一些动画函数和更新控制器                       ,相当于 Three.js 中用原生实现的 requestAnimationFrame;useLoader 用于加载器的管理                ,使用它更方便进行加载错误管理和回调方法执行;lerp 是一个插值运算函数,它可以计算某一数值到另一数值的百分比                       ,从而得出一个新的数值                        ,常用于移动物体       、修改透明度        、颜色                        、大小               、模拟动画等                       。

import { Physics, useBox, usePlane, useSphere } from "@react-three/cannon"; import { useFrame, useLoader } from "@react-three/fiber"; import { Mesh, TextureLoader } from "three"; import { GLTFLoader } from "three-stdlib/loaders/GLTFLoader"; import lerp from "lerp"; 创建物理世界

然后创建一个 PingPong 类,在其中添加 <Physics> 组件来创建物理世界               ,像直接使用 Cannon.js 一样                        ,可以给它设置 iterations        、tolerance                        、gravity               、allowSleep 等参数来分别设置物理世界的迭代次数、容错性                        、引力以及是否支持进入休眠状态等        ,然后在其中添加一个平面几何体和一个平面刚体 ContactGround        。

function ContactGround() { const [ref] = usePlane( () => ({ position: [0, -10, 0], rotation: [-Math.PI / 2, 0, 0], type: "Static", }), useRef < Mesh > null ); return <mesh ref={ref} />; } export default function PingPong() { return ( <> <Physics iterations={20} tolerance={0.0001} defaultContactMaterial={{ contactEquationRelaxation: 1, contactEquationStiffness: 1e7, friction: 0.9, frictionEquationRelaxation: 2, frictionEquationStiffness: 1e7, restitution: 0.7, }} gravity={[0, -40, 0]} allowSleep={false} > <mesh position={[0, 0, -10]} receiveShadow> <planeGeometry args={[1000, 1000]} /> <meshPhongMaterial /> </mesh> <ContactGround /> </Physics> </> ); } 创建乒乓球

接着               ,我们创建一个球体类 Ball                       ,在其中添加球体 🟡         ,可以使用前面介绍的 useLoader 来管理它的贴图加载        ,为了方便观察到乒乓球的转动情况                       ,贴图中央加了一个十字交叉图案 ➕        。然后将其放在 <Physics> 标签下                       。

function Ball() { const map = useLoader(TextureLoader, earthImg); const [ref] = useSphere( () => ({ args: [0.5], mass: 1, position: [0, 5, 0] }), useRef < Mesh > null ); return ( <mesh castShadow ref={ref}> <sphereGeometry args={[0.5, 64, 64]} /> <meshStandardMaterial map={map} /> </mesh> ); } export default function PingPong() { return ( <> <Physics> { /* ... */ } <Ball /> </Physics> </> ); } 创建球拍

球拍 🏓 采用的是一个 glb 格式的模型                ,在 Blender 中我们可以看到模型的样式和详细的骨骼结构        ,对于模型的加载                       ,我们同样使用 useLoader 来管理                ,此时的加载器需要使用 GLTFLoader                。

我们创建一个 Paddle 类并将其添加到 <Physics> 标签中,在这个类中我们实现模型加载                       ,模型加载完成后绑定骨骼                        ,并在 useFrame 页面重绘方法中,根据鼠标所在位置更新乒乓球拍模型的位置 position               ,并根据是否一开始游戏状态以及鼠标的位置来更新球拍的 x轴 和 y轴 方向的 rotation 值        。

function Paddle() { const { nodes, materials } = useLoader( GLTFLoader, /models/pingpong.glb, ); const model = useRef(); const [ref, api] = useBox(() => ({ type: Kinematic, args: [3.4, 1, 3.5], })); const values = useRef([0, 0]); useFrame((state) => { values.current[0] = lerp( values.current[0], (state.mouse.x * Math.PI) / 5, 0.2 ); values.current[1] = lerp( values.current[1], (state.mouse.x * Math.PI) / 5, 0.2 ); api.position.set(state.mouse.x * 10, state.mouse.y * 5, 0); api.rotation.set(0, 0, values.current[1]); if (!model.current) return; model.current.rotation.x = lerp( model.current.rotation.x, started ? Math.PI / 2 : 0, 0.2 ); model.current.rotation.y = values.current[0]; }); return ( <mesh ref={ref} dispose={null}> <group ref={model} position={[-0.05, 0.37, 0.3]} scale={[0.15, 0.15, 0.15]} > <group rotation={[1.88, -0.35, 2.32]} scale={[2.97, 2.97, 2.97]}> <primitive object={nodes.Bone} /> <primitive object={nodes.Bone003} /> { /* ... */ } <skinnedMesh castShadow receiveShadow material={materials.glove} material-roughness={1} geometry={nodes.arm.geometry} skeleton={nodes.arm.skeleton} /> </group> <group rotation={[0, -0.04, 0]} scale={[141.94, 141.94, 141.94]}> <mesh castShadow receiveShadow material={materials.wood} geometry={nodes.mesh.geometry} /> { /* ... */ } </group> </group> </mesh> ); }

到这里                        ,我们已经实现乒乓球颠球的基本功能了 🤩

颠球计数

为了显示每次游戏可以颠球的次数        ,现在我们在乒乓球拍中央加上数字显示 5️⃣                        。我们可以像下面这样创建一个 Text 类               ,在文件顶部引入 TextGeometry                       、FontLoader、fontJson 作为字体几何体                、字体加载器以及字体文件                       ,添加一个 geom 作为创建字体几何体的方法        ,当 count 状态值发生变化时        ,实时更新创建字体几何体模型                。

import { useMemo } from "react"; import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry"; import { FontLoader } from "three/examples/jsm/loaders/FontLoader"; import fontJson from "../public/fonts/firasans_regular.json"; const font = new FontLoader().parse(fontJson); const geom = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map( (number) => new TextGeometry(number, { font, height: 0.1, size: 5 }) ); export default function Text({ color = 0xffffff, count, ...props }) { const array = useMemo(() => [...count], [count]); return ( <group {...props} dispose={null}> {array.map((char, index) => ( <mesh position={[-(array.length / 2) * 3.5 + index * 3.5, 0, 0]} key={index} geometry={geom[parseInt(char)]} > <meshBasicMaterial color={color} transparent opacity={0.5} /> </mesh> ))} </group> ); }

然后将 Text 字体类放入球拍几何体中                       ,其中 count 字段需要在物理世界中刚体发生碰撞时进行更新                ,该方法加载下节内容添加碰撞音效时一起实现。

function Paddle() { return ( <mesh ref={ref} dispose={null}> <group ref={model}> { /* ... */ } <Text rotation={[-Math.PI / 2, 0, 0]} position={[0, 1, 2]} count={count.toString()} /> </group> </mesh> ); }

④ 页面装饰

到这里        ,整个小游戏的全部流程都开发完毕了                       ,现在我们来加一些页面提示语                       、颠球时的碰撞音效                ,页面的光照效果等,使 3D 场景看起来更加真实                       。

音效

实现音效 🔈 前                       ,我们先像下面这样添加一个状态管理器 📦                         ,来进行页面全局状态的管理                        。zustand 是一个轻量级的状态管理库;_.clamp(number, [lower], upper) 用于返回限制在 lower 和 upper 之间的值;pingSound 是需要播放的音频文件。我们在其中添加一个 pong 方法用来更新音效颠球计数,添加一个 reset 方法重置颠球数字               。count 字段表示每次的颠球次数               ,welcome 表示是否在欢迎界面                        。

import create from "zustand"; import clamp from "lodash-es/clamp"; import pingSound from "/medias/ping.mp3"; const ping = new Audio(pingSound); export const useStore = create((set) => ({ api: { pong(velocity) { ping.currentTime = 0; ping.volume = clamp(velocity / 20, 0, 1); ping.play(); if (velocity > 4) set((state) => ({ count: state.count + 1 })); }, reset: (welcome) => set((state) => ({ count: welcome ? state.count : 0, welcome })), }, count: 0, welcome: true, }));

然后我们可以在上述 Paddle 乒乓球拍类中像这样在物体发生碰撞时触发 pong 方法:

function Paddle() { {/* ... */} const [ref, api] = useBox(() => ({ type: "Kinematic", args: [3.4, 1, 3.5], onCollide: (e) => pong(e.contact.impactVelocity), })); } 光照

为了是场景更加真实                        ,我们可以开启 Canvas 的阴影        ,然后添加多种光源 💡 来优化场景               ,如 spotLight 就能起到视觉聚焦的作用        。

<Canvas shadows camera={{ fov: 50, position: [0, 5, 12] }} > <ambientLight intensity={.5} /> <pointLight position={[-10, -10, -10]} /> <spotLight position={[10, 10, 10]} angle={0.3} penumbra={1} intensity={1} castShadow shadow-mapSize-width={2048} shadow-mapSize-height={2048} shadow-bias={-0.0001} /> <PingPong /> </Canvas> 提示语

为了提升小游戏的用户体验                       ,我们可以添加一些页面文字提示来指引使用者和提升页面视觉效果        ,需要注意的是        ,这些额外的元素不能添加到 <Canvas /> 标签内哦 😄               。

const style = (welcome) => ({ color: #000000, display: welcome ? block : none, fontSize: 1.8em, left: 50%, position: "absolute", top: 40, transform: translateX(-50%), background: rgba(255, 255, 255, .2), backdropFilter: blur(4px), padding: 16px, borderRadius: 12px, boxShadow: 1px 1px 2px rgba(0, 0, 0, .2), border: 1px groove rgba(255, 255, 255, .2), textShadow: 0px 1px 2px rgba(255, 255, 255, .2), 0px 2px 2px rgba(255, 255, 255, .8), 0px 2px 4px rgba(0, 0, 0, .5) }); <div style={style(welcome)}>🏓 点击任意区域开始颠球</div>

🔗 源码地址: https://github.com/dragonir/threejs-odessey

总结

本文中主要包含的知识点包括:

了解什么是 React Three Fiber 及相关生态                       。 React Three Fiber 基础入门        。 使用 React Three Fiber 开发一个乒乓球小游戏                       ,学会如何场景构建       、模型加载                、物理世界关联                        、全局状态管理等        。

想了解其他前端知识或其他未在本文中详细描述的Web 3D开发技术相关知识                ,可阅读我往期的文章                       。如果有疑问可以在评论中留言        ,如果觉得文章对你有帮助                       ,不要忘了一键三连哦 👍                。

附录

[1]. 🌴 Three.js 打造缤纷夏日3D梦中情岛 [2]. 🔥 Three.js 实现炫酷的赛博朋克风格3D数字地球大屏 [3]. 🐼 Three.js 实现2022冬奥主题3D趣味页面                ,含冰墩墩 [4]. 🦊 Three.js 实现3D开放世界小游戏:阿狸的多元宇宙 [5]. 🏆 掘金1000粉!使用Three.js实现一个创意纪念页面 ... 【Three.js 进阶之旅】系列专栏访问 👈 更多往期【3D】专栏访问 👈 更多往期【前端】专栏访问 👈

参考

[1]. React Three Fiber [2]. threejs.org

本文作者:dragonir 本文地址:https://www.cnblogs.com/dragonir/p/17235128.html

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
影响百度蜘蛛数量的因素剖析(如何掌握百度蜘蛛爬行机制?) 苹果cms10插件(2022年苹果CMSV10系统Java萝卜影视源码完美修复完整版-OK源码破解)