Three.js實(shí)現(xiàn)3D乒乓球小游戲(物理效果)
本文將使用React Three Fiber 和 Cannon.js 來(lái)實(shí)現(xiàn)一個(gè)具有物理特性的乒乓球小游戲,通過(guò)本文的閱讀,你將學(xué)習(xí)到的知識(shí)點(diǎn)包括:了解什么是 React Three Fiber 及它的相關(guān)生態(tài)、使用 React Three Fiber 搭建基礎(chǔ)三維場(chǎng)景、如何使用新技術(shù)棧給場(chǎng)景中對(duì)象的添加物理特性等,最后利用上述知識(shí)點(diǎn),將開發(fā)一個(gè)簡(jiǎn)單的乒乓球小游戲。
聲明:本文涉及圖文和模型素材僅用于個(gè)人學(xué)習(xí)、研究和欣賞,請(qǐng)勿二次修改、非法傳播、轉(zhuǎn)載、出版、商用、及進(jìn)行其他獲利行為。
摘要
本文在專欄上一篇內(nèi)容《three.js利用射線Raycaster進(jìn)行碰撞檢測(cè)》的基礎(chǔ)上,將使用新的技術(shù)棧 React Three Fiber
和 Cannon.js
來(lái)實(shí)現(xiàn)一個(gè)具有物理特性的小游戲,通過(guò)本文的閱讀,你將學(xué)習(xí)到的知識(shí)點(diǎn)包括:了解什么是 React Three Fiber
及它的相關(guān)生態(tài)、使用 React Three Fiber
搭建基礎(chǔ)三維場(chǎng)景、如何使用新技術(shù)棧給場(chǎng)景中對(duì)象的添加物理特性等,最后利用上述知識(shí)點(diǎn),將開發(fā)一個(gè)簡(jiǎn)單的乒乓球小游戲。
效果
在正式學(xué)習(xí)之前,我們先來(lái)看看本文示例最終實(shí)現(xiàn)效果:頁(yè)面主體內(nèi)容是一個(gè)手握乒乓球拍的模型和一個(gè)乒乓球 ??
,對(duì)球拍像現(xiàn)實(shí)生活中一樣進(jìn)行顛球施力操作,乒乓球可以在球拍上彈起,乒乓球彈起的高度隨著施加在球拍上的力的大小的變化而變化,球拍中央顯示的是連續(xù)顛球次數(shù) 5??
,當(dāng)乒乓球從球拍掉落時(shí)一局游戲結(jié)束,球拍上的數(shù)字歸零 0??
??靵?lái)試試你一次可以顛多少個(gè)球吧 ??
。
打開以下鏈接,在線預(yù)覽效果,大屏訪問(wèn)效果更佳。
????
在線預(yù)覽地址:https://dragonir.github.io/physics-pingpong/
本專欄系列代碼托管在 Github
倉(cāng)庫(kù)【threejs-odessey】,后續(xù)所有目錄也都將在此倉(cāng)庫(kù)中更新。
??
代碼倉(cāng)庫(kù)地址:git@github.com:dragonir/threejs-odessey.git
原理
React-Three-Fiber
React Three Fiber
是一個(gè)基于 Three.js
的 React
渲染器,簡(jiǎn)稱 R3F
。它像是一個(gè)配置器,把 Three.js
的對(duì)象映射為 R3F
中的組件。以下是一些相關(guān)鏈接:
倉(cāng)庫(kù): https://github.com/pmndrs/react-three-fiber
官網(wǎng): https://docs.pmnd.rs/react-three-fiber/getting-started/introduction
示例: https://docs.pmnd.rs/react-three-fiber/getting-started/examples
特點(diǎn)
- 使用可重用的組件以聲明方式構(gòu)建動(dòng)態(tài)場(chǎng)景圖,使
Three.js
的處理變得更加輕松,并使代碼庫(kù)更加整潔。這些組件對(duì)狀態(tài)變化做出反應(yīng),具有開箱即用的交互性。 Three.js
中所有內(nèi)容都能在這里運(yùn)行。它不針對(duì)特定的Three.js
版本,也不需要更新以修改,添加或刪除上游功能。- 渲染性能與
Three.js
和GPU
相仿。組件參與React
之外的render loop
時(shí),沒(méi)有任何額外開銷。
寫 React Three Fiber
比較繁瑣,我們可以寫成 R3F
或簡(jiǎn)稱為 Fiber
。讓我們從現(xiàn)在開始使用 R3F
吧。
生態(tài)系統(tǒng)
R3F
有充滿活力的生態(tài)系統(tǒng),包括各種庫(kù)、輔助工具以及抽象方法:
@react-three/drei
– 有用的輔助工具,自身就有豐富的生態(tài)@react-three/gltfjsx
– 將GLTFs
轉(zhuǎn)換為JSX
組件@react-three/postprocessing
– 后期處理效果@react-three/test-renderer
– 用于在Node
中進(jìn)行單元測(cè)試@react-three/flex
–react-three-fiber
的flex
盒子布局@react-three/xr
–VR/AR
控制器和事件@react-three/csg
– 構(gòu)造實(shí)體幾何@react-three/rapier
– 使用Rapier
的3D
物理引擎@react-three/cannon
– 使用Cannon
的3D
物理引擎@react-three/p2
– 使用P2
的2D
物理引擎@react-three/a11y
– 可訪問(wèn)工具@react-three/gpu-pathtracer
– 真實(shí)的路徑追蹤create-r3f-app next
–nextjs
啟動(dòng)器lamina
– 基于shader materials
的圖層zustand
– 基于flux
的狀態(tài)管理jotai
– 基于atoms
的狀態(tài)管理valtio
– 基于proxy
的狀態(tài)管理react-spring
– 一個(gè)spring-physics-based
的動(dòng)畫庫(kù)framer-motion-3d
–framer motion
,一個(gè)很受歡迎的動(dòng)畫庫(kù)use-gesture
– 鼠標(biāo)/觸摸手勢(shì)leva
– 創(chuàng)建GUI
控制器maath
– 數(shù)學(xué)輔助工具miniplex
–ECS
實(shí)體管理系統(tǒng)composer-suite
– 合成著色器、粒子、特效和游戲機(jī)制、
安裝
npm install three @react-three/fiber
第一個(gè)場(chǎng)景
在一個(gè)新建的 React
項(xiàng)目中,我們通過(guò)以下的步驟使用 R3F
來(lái)創(chuàng)建第一個(gè)場(chǎng)景。
初始化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
組件在幕后做了一些重要的初始化工作:
- 它初始化了一個(gè)場(chǎng)景
Scene
和一個(gè)相機(jī)Camera
,它們都是渲染所需的基本模塊。 - 它在頁(yè)面每一幀更新中都渲染場(chǎng)景,我們不需要再到頁(yè)面重繪方法中循環(huán)調(diào)用渲染方法。
??
Canvas 大小響應(yīng)式自適應(yīng)于父節(jié)點(diǎn),我們可以通過(guò)改變父節(jié)點(diǎn)的寬度和高度來(lái)控制渲染場(chǎng)景的尺寸大小。
添加一個(gè)Mesh組件
為了真正能夠在場(chǎng)景中看到一些物體,現(xiàn)在我們添加一個(gè)小寫的 <mesh />
元素,它直接等效于 new THREE.Mesh()
。
<Canvas> <mesh />
??
可以看到我們沒(méi)有特地去額外引入mesh組件,我們不需要引入任何元素,所有Three.js中的對(duì)象都將被當(dāng)作原生的JSX元素,就像在 ReactDom
中寫 <div />
及 <span />
元素一樣。R3F Fiber組件的通用規(guī)則是將Three.js中的它們的名字寫成駝峰式的DOM元素即可。
一個(gè) Mesh
是 Three.js
中的基礎(chǔ)場(chǎng)景對(duì)象,需要給它提供一個(gè)幾何對(duì)象 geometry
以及一個(gè)材質(zhì) material
來(lái)代表一個(gè)三維空間的幾何形狀,我們將使用一個(gè) BoxGeometry
和 MeshStandardMaterial
來(lái)創(chuàng)建一個(gè)新的網(wǎng)格 Mesh
,它們會(huì)自動(dòng)關(guān)聯(lián)到它們的父節(jié)點(diǎn)。
<Canvas> <mesh> <boxGeometry /> <meshStandardMaterial /> </mesh>
上述代碼和以下 Three.js
代碼是等價(jià)的:
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()
構(gòu)造函數(shù)參數(shù):
根據(jù) BoxGeometry
的文檔,我們可以選擇給它傳遞三個(gè)參數(shù):width
、length
及 depth
:
new THREE.BoxGeometry(2, 2, 2)
為了實(shí)現(xiàn)相同的功能,我們可以在 R3F
中使用 args
屬性,它總是接受一個(gè)數(shù)組,其項(xiàng)目表示構(gòu)造函數(shù)參數(shù):
<boxGeometry args={[2, 2, 2]} />
添加光源
接著,我們通過(guò)像下面這樣添加光源組件來(lái)為我們的場(chǎng)景添加一些光線。
<Canvas> <ambientLight intensity={0.1} /> <directionalLight color="red" position={[0, 0, 5]} />
屬性:
這里介紹關(guān)于 R3F
的最后一個(gè)概念,即 React
屬性是如何在 Three.js
對(duì)象中工作的。當(dāng)你給一個(gè) Fiber
組件設(shè)置任意屬性時(shí),它將對(duì) Three.js
設(shè)置一個(gè)相同名字的屬性。我們關(guān)注到 ambientLight
上,由它的文檔可知,我們可以選擇 color
和 intensity
屬性來(lái)初始化它:
<ambientLight intensity={0.1} />
等價(jià)于
const light = new THREE.AmbientLight() light.intensity = 0.1
快捷方法:
在 Three.js
中對(duì)于很多屬性的設(shè)置如 colors
、vectors
等都可以使用 set()
方法進(jìn)行快捷設(shè)置:
const light = new THREE.DirectionalLight() light.position.set(0, 0, 5) light.color.set('red')
在 JSX
中也是相同的:
<directionalLight position={[0, 0, 5]} color="red" />
結(jié)果
<Canvas> <mesh> <boxBufferGeometry /> <meshBasicMaterial color="#03c03c" /> </mesh> <ambientLight args={[0xff0000]} intensity={0.1} /> <directionalLight position={[0, 0, 5]} intensity={0.5} /> </Canvas>
查看React Three Fiber完整API文檔
實(shí)現(xiàn)
到這里,我們已經(jīng)掌握了 R3F
的基本知識(shí),我們?cè)俳Y(jié)合專欄上篇關(guān)于物理特性的內(nèi)容,來(lái)實(shí)現(xiàn)如文章開頭介紹的乒乓球 ??
小游戲。
??
本文乒乓球小游戲基礎(chǔ)版及乒乓球三維模型資源來(lái)源于R3F官網(wǎng)示例。
〇 搭建頁(yè)面基本結(jié)構(gòu)
首先,我們創(chuàng)建一個(gè) Experience
文件作為渲染三維場(chǎng)景的組件,并在其中添加 Canvas
組件搭建基本頁(yè)面結(jié)構(gòu)。
import { Canvas } from "@react-three/fiber"; export default function Experience() { return ( <> <Canvas></Canvas> </> ); }
① 場(chǎng)景初始化
接著我們開啟 Canvas
的陰影并設(shè)置相機(jī)參數(shù),然后添加環(huán)境光 ambientLight
和點(diǎn)光源 pointLight
兩種光源:
<Canvas shadows camera={{ fov: 50, position: [0, 5, 12] }} > <ambientLight intensity={.5} /> <pointLight position={[-10, -10, -10]} /> </Canvas>
如果需要修改 Canvas
的背景色,可以在其中添加一個(gè) color
標(biāo)簽并設(shè)置參數(shù) attach
為 background
,在 args
參數(shù)中設(shè)置顏色即可。
<Canvas> <color attach="background" args={["lightgreen"]} /> </Canvas>
② 添加輔助工具
接著,我們?cè)陧?yè)面頂部引入 Perf
,它是 R3F
生態(tài)中查看頁(yè)面性能的組件,它的功能和 Three.js
中 stats.js
是類似的,像下面這樣添加到代碼中設(shè)置它的顯示位置,頁(yè)面對(duì)應(yīng)區(qū)域就會(huì)出現(xiàn)可視化的查看工具,在上面可以查看 GPU
、CPU
、FPS
等性能參數(shù)。
如果想使用網(wǎng)格作為輔助線或用作裝飾,可以使用 gridHelper
組件,它支持配置 position
、rotation
、args
等參數(shù)。
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> </> ); }
③ 創(chuàng)建乒乓球和球拍
我們創(chuàng)建一個(gè)名為 PingPong.jsx
的乒乓球組件文件,然后在文件頂部引入以下依賴,其中 Physics
、useBox
、usePlane
、useSphere
用于創(chuàng)建物理世界;useFrame
是用來(lái)進(jìn)行頁(yè)面動(dòng)畫更新的 hook
,它將在頁(yè)面每幀重繪時(shí)執(zhí)行,我們可以在它里面執(zhí)行一些動(dòng)畫函數(shù)和更新控制器,相當(dāng)于 Three.js
中用原生實(shí)現(xiàn)的 requestAnimationFrame
;useLoader
用于加載器的管理,使用它更方便進(jìn)行加載錯(cuò)誤管理和回調(diào)方法執(zhí)行;lerp
是一個(gè)插值運(yùn)算函數(shù),它可以計(jì)算某一數(shù)值到另一數(shù)值的百分比,從而得出一個(gè)新的數(shù)值,常用于移動(dòng)物體、修改透明度、顏色、大小、模擬動(dòng)畫等。
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";
創(chuàng)建物理世界
然后創(chuàng)建一個(gè) PingPong
類,在其中添加 <Physics>
組件來(lái)創(chuàng)建物理世界,像直接使用 Cannon.js
一樣,可以給它設(shè)置 iterations
、tolerance
、gravity
、allowSleep
等參數(shù)來(lái)分別設(shè)置物理世界的迭代次數(shù)、容錯(cuò)性、引力以及是否支持進(jìn)入休眠狀態(tài)等,然后在其中添加一個(gè)平面幾何體和一個(gè)平面剛體 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 color="#5081ca" /> </mesh> <ContactGround /> </Physics> </> ); }
創(chuàng)建乒乓球
接著,我們創(chuàng)建一個(gè)球體類 Ball
,在其中添加球體 ??
,可以使用前面介紹的 useLoader
來(lái)管理它的貼圖加載,為了方便觀察到乒乓球的轉(zhuǎn)動(dòng)情況,貼圖中央加了一個(gè)十字交叉圖案 ?
。然后將其放在 <Physics>
標(biāo)簽下。
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> </> ); }
創(chuàng)建球拍
球拍 ??
采用的是一個(gè) glb
格式的模型,在 Blender
中我們可以看到模型的樣式和詳細(xì)的骨骼結(jié)構(gòu),對(duì)于模型的加載,我們同樣使用 useLoader
來(lái)管理,此時(shí)的加載器需要使用 GLTFLoader
。
我們創(chuàng)建一個(gè) Paddle
類并將其添加到 <Physics>
標(biāo)簽中,在這個(gè)類中我們實(shí)現(xiàn)模型加載,模型加載完成后綁定骨骼,并在 useFrame
頁(yè)面重繪方法中,根據(jù)鼠標(biāo)所在位置更新乒乓球拍模型的位置 position
,并根據(jù)是否一開始游戲狀態(tài)以及鼠標(biāo)的位置來(lái)更新球拍的 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> ); }
到這里,我們已經(jīng)實(shí)現(xiàn)乒乓球顛球的基本功能了 ??
顛球計(jì)數(shù)
為了顯示每次游戲可以顛球的次數(shù),現(xiàn)在我們?cè)谄古仪蚺闹醒爰由蠑?shù)字顯示 5??
。我們可以像下面這樣創(chuàng)建一個(gè) Text
類,在文件頂部引入 TextGeometry
、FontLoader
、fontJson
作為字體幾何體、字體加載器以及字體文件,添加一個(gè) geom
作為創(chuàng)建字體幾何體的方法,當(dāng) count
狀態(tài)值發(fā)生變化時(shí),實(shí)時(shí)更新創(chuàng)建字體幾何體模型。
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
字段需要在物理世界中剛體發(fā)生碰撞時(shí)進(jìn)行更新,該方法加載下節(jié)內(nèi)容添加碰撞音效時(shí)一起實(shí)現(xiàn)。
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> ); }
④ 頁(yè)面裝飾
到這里,整個(gè)小游戲的全部流程都開發(fā)完畢了,現(xiàn)在我們來(lái)加一些頁(yè)面提示語(yǔ)、顛球時(shí)的碰撞音效,頁(yè)面的光照效果等,使 3D
場(chǎng)景看起來(lái)更加真實(shí)。
音效
實(shí)現(xiàn)音效 ??
前,我們先像下面這樣添加一個(gè)狀態(tài)管理器 ??
,來(lái)進(jìn)行頁(yè)面全局狀態(tài)的管理。zustand
是一個(gè)輕量級(jí)的狀態(tài)管理庫(kù);_.clamp(number, [lower], upper)
用于返回限制在 lower
和 upper
之間的值;pingSound
是需要播放的音頻文件。我們?cè)谄渲刑砑右粋€(gè) pong
方法用來(lái)更新音效和顛球計(jì)數(shù),添加一個(gè) reset
方法重置顛球數(shù)字。count
字段表示每次的顛球次數(shù),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
乒乓球拍類中像這樣在物體發(fā)生碰撞時(shí)觸發(fā) pong
方法:
function Paddle() { {/* ... */} const [ref, api] = useBox(() => ({ type: "Kinematic", args: [3.4, 1, 3.5], onCollide: (e) => pong(e.contact.impactVelocity), })); }
光照
為了是場(chǎng)景更加真實(shí),我們可以開啟 Canvas
的陰影,然后添加多種光源 ??
來(lái)優(yōu)化場(chǎng)景,如 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>
提示語(yǔ)
為了提升小游戲的用戶體驗(yàn),我們可以添加一些頁(yè)面文字提示來(lái)指引使用者和提升頁(yè)面視覺效果,需要注意的是,這些額外的元素不能添加到 <Canvas />
標(biāo)簽內(nèi)哦 ??
。
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)}>?? 點(diǎn)擊任意區(qū)域開始顛球</div>
??
源碼地址: https://github.com/dragonir/threejs-odessey
總結(jié)
本文中主要包含的知識(shí)點(diǎn)包括:
- 了解什么是
React Three Fiber
及相關(guān)生態(tài)。 React Three Fiber
基礎(chǔ)入門。- 使用
React Three Fiber
開發(fā)一個(gè)乒乓球小游戲,學(xué)會(huì)如何場(chǎng)景構(gòu)建、模型加載、物理世界關(guān)聯(lián)、全局狀態(tài)管理等。
到此這篇關(guān)于Three.js實(shí)現(xiàn)3D乒乓球小游戲(物理效果)的文章就介紹到這了,更多相關(guān)Three.js 3D乒乓球小游戲內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JS實(shí)現(xiàn)可編輯的后臺(tái)管理菜單功能【附demo源碼下載】
這篇文章主要介紹了JS實(shí)現(xiàn)可編輯的后臺(tái)管理菜單功能,涉及javascript針對(duì)頁(yè)面元素的遍歷及動(dòng)態(tài)修改相關(guān)操作技巧,并附帶demo源碼供讀者下載參考,需要的朋友可以參考下2016-09-09JavaScript中函數(shù)聲明與函數(shù)表達(dá)式的區(qū)別詳解
可能很多朋友只知道兩種聲明方式一個(gè)是函數(shù)聲明一個(gè)是函數(shù)表達(dá)式,具體有什么不同沒(méi)能說(shuō)得很好。事實(shí)上,JavaScript的解析器對(duì)函數(shù)聲明與函數(shù)表達(dá)式并不是一視同仁地對(duì)待的。下面看看這兩者到底有什么不同。2016-08-08JavaScript延遲加載之a(chǎn)sync與defer的應(yīng)用
這篇文章主要介紹了JavaScript延遲加載之a(chǎn)sync與defer的應(yīng)用場(chǎng)景與使用區(qū)別的介紹,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-09-09uni-app多環(huán)境配置實(shí)現(xiàn)自動(dòng)部署的方式詳解
前后端分離開發(fā)模式中,無(wú)論前后端都有可能區(qū)分不同的環(huán)境配置,下面這篇文章主要給大家介紹了關(guān)于uni-app多環(huán)境配置實(shí)現(xiàn)自動(dòng)部署的相關(guān)資料,需要的朋友可以參考下2022-06-06JavaScript學(xué)習(xí)筆記之?dāng)?shù)組的增、刪、改、查
這篇文章主要介紹了JavaScript學(xué)習(xí)筆記之?dāng)?shù)組的增、刪、改、查的相關(guān)資料,需要的朋友可以參考下2016-03-03JavaScript實(shí)現(xiàn)的encode64加密算法實(shí)例分析
這篇文章主要介紹了JavaScript實(shí)現(xiàn)的encode64加密算法,實(shí)例分析了javascript處理encode64編碼針對(duì)字符串加密的技巧,非常簡(jiǎn)潔實(shí)用,需要的朋友可以參考下2015-04-04javascript自動(dòng)切換焦點(diǎn)控制效果完整實(shí)例
這篇文章主要介紹了javascript自動(dòng)切換焦點(diǎn)控制效果的方法,結(jié)合完整實(shí)例形式分析了JavaScript響應(yīng)鍵盤按鍵控制表單輸入框的焦點(diǎn)切換功能,需要的朋友可以參考下2016-02-02js substr支持中文截取函數(shù)代碼(中文是雙字節(jié))
js substr支持中文截取函數(shù)代碼,中文是雙字節(jié),配有實(shí)例需要的朋友可以參考下2013-04-04