温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

怎么使用Three.js实现3D乒乓球小游戏

发布时间:2023-03-21 11:33:21 阅读:217 作者:iii 栏目:开发技术
前端开发者测试专用服务器限时活动,0元免费领,库存有限,领完即止! 点击查看>>

本篇内容介绍了“怎么使用Three.js实现3D乒乓球小游戏”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

效果

怎么使用Three.js实现3D乒乓球小游戏

原理

React-Three-Fiber

React Three Fiber 是一个基于 Three.jsReact 渲染器,简称 R3F。它像是一个配置器,把 Three.js 的对象映射为 R3F 中的组件。

特点
  • 使用可重用的组件以声明方式构建动态场景图,使 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元素即可。

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

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

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

const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, width / height, 0.11000)

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 的文档,我们可以选择给它传递三个参数:widthlengthdepth

new THREE.BoxGeometry(222)

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

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

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

<Canvas>
  <ambientLight intensity={0.1} />
  <directionalLight color="red" position={[0, 05]} />

属性:

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

<ambientLight intensity={0.1} />

等价于

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

快捷方法:

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

const light = new THREE.DirectionalLight()
light.position.set(005)
light.color.set('red')

JSX 中也是相同的:

<directionalLight position={[0, 05]} color="red" />
结果
<Canvas>
  <mesh>
    <boxBufferGeometry />
    <meshBasicMaterial color="#03c03c" />
  </mesh>
  <ambientLight args={[0xff0000]} intensity={0.1} />
  <directionalLight position={[0, 05]} intensity={0.5} />
</Canvas>

怎么使用Three.js实现3D乒乓球小游戏

查看React Three Fiber完整API文档

实现

〇 搭建页面基本结构

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

import { Canvas } from "@react-three/fiber";

export default function Experience() {
  return (
    <>
      <Canvas></Canvas>
    </>
  );
}

怎么使用Three.js实现3D乒乓球小游戏

① 场景初始化

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

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

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

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

怎么使用Three.js实现3D乒乓球小游戏

② 添加辅助工具

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

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

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.6800]} />
      </Canvas>
    </>
  );
}

怎么使用Three.js实现3D乒乓球小游戏

③ 创建乒乓球和球拍

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

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

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

function ContactGround() {
  const [ref] = usePlane(
    () => ({
      position: [0, -100],
      rotation: [-Math.PI / 200],
      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, -400]}
        allowSleep={false}
      >
        <mesh position={[0, 0-10]} receiveShadow>
          <planeGeometry args={[1000, 1000]} />
          <meshPhongMaterial color="#5081ca" />
        </mesh>
        <ContactGround />
      </Physics>
    </>
  );
}

怎么使用Three.js实现3D乒乓球小游戏

创建乒乓球

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

function Ball() {
  const map = useLoader(TextureLoader, earthImg);
  const [ref] = useSphere(
    () => ({ args: [0.5], mass1position: [050] }),
    useRef < Mesh > null
  );
  return (
    <mesh castShadow ref={ref}>
      <sphereGeometry args={[0.5, 6464]} />
      <meshStandardMaterial map={map} />
    </mesh>
  );
}

export default function PingPong() {
  return (
    <>
      <Physics>
        { /* ... */ }
        <Ball />
      </Physics>
    </>
  );
}

怎么使用Three.js实现3D乒乓球小游戏

创建球拍

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

怎么使用Three.js实现3D乒乓球小游戏

我们创建一个 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.413.5],
  }));
  const values = useRef([00]);
  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 * 50);
    api.rotation.set(00, values.current[1]);
    if (!model.currentreturn;
    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.370.3]}
        scale={[0.15, 0.150.15]}
      >
        <group rotation={[1.88, -0.352.32]} scale={[2.97, 2.972.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.040]} scale={[141.94, 141.94141.94]}>
          <mesh
            castShadow
            receiveShadow
            material={materials.wood}
            geometry={nodes.mesh.geometry}
          />
          { /* ... */ }
        </group>
      </group>
    </mesh>
  );
}

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

怎么使用Three.js实现3D乒乓球小游戏

颠球计数

为了显示每次游戏可以颠球的次数,现在我们在乒乓球拍中央加上数字显示 5️⃣ 。我们可以像下面这样创建一个 Text 类,在文件顶部引入 TextGeometryFontLoaderfontJson 作为字体几何体、字体加载器以及字体文件,添加一个 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, height0.1size5 })
);

export default function Text({ color = 0xffffff, count, ...props }) {
  const array = useMemo(() => [...count], [count]);
  return (
    <group {...propsdispose={null}>
      {array.map((char, index) => (
        <mesh
          position={[-(array.length / 2) * 3.5 + index * 3.500]}
          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 / 200]}
          position={[0, 12]}
          count={count.toString()}
        />
      </group>
    </mesh>
  );
}

怎么使用Three.js实现3D乒乓球小游戏

④ 页面装饰

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

音效

实现音效前,我们先像下面这样添加一个状态管理器,来进行页面全局状态的管理。zustand 是一个轻量级的状态管理库;_.clamp(number, [lower], upper) 用于返回限制在 lowerupper 之间的值;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 / 2001);
      ping.play();
      if (velocity > 4set((state) => ({ count: state.count + 1 }));
    },
    reset(welcome) =>
      set((state) => ({ count: welcome ? state.count : 0, welcome })),
  },
  count0,
  welcometrue,
}));

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

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

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

<Canvas
  shadows
  camera={{ fov: 50position: [0512] }}
>
  <ambientLight intensity={.5} />
  <pointLight position={[-10, -10-10]} />
  <spotLight
    position={[10, 1010]}
    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",
  top40,
  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>

怎么使用Three.js实现3D乒乓球小游戏

“怎么使用Three.js实现3D乒乓球小游戏”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!

亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

原文链接:https://www.cnblogs.com/dragonir/p/17235128.html

AI

开发者交流群×