帝國cms做笑話網(wǎng)站宣傳渠道和宣傳方式有哪些
最近在看three.js相關(guān)的東西,想著學(xué)習(xí)一下threejs給的examples。源碼是用html結(jié)合js寫的,恰好最近也在學(xué)習(xí)react,就用react框架學(xué)習(xí)一下。
本文參考的是threeJs給的第一個示例
?three.js examples (threejs.org)
?一、下載threeJS源碼
通常我們只用通過npm引入threejs的包就可以使用threejs了。為什么這里需要下載源碼呢?因為我們要復(fù)刻源碼給的示例,相關(guān)的模型我們是沒有的,需要使用源碼里用到的模型及解析工具
GitHub - mrdoob/three.js: JavaScript 3D Library.
?從git上拉取代碼后可以找到示例一的源碼
?閱讀源碼可以發(fā)現(xiàn),完成示例需要引入jsm/libs/draco/gltf/路徑以及models/gltf/LittlestTokyo.glb模型。
拷貝threeJS的必要的模型和方法
為了方便后續(xù)學(xué)習(xí),我們直接將這兩個文件夾jsm和models拷貝到react項目中;注意路徑最好是public下,public是默認的靜態(tài)資源加載入口
?
?二、功能解析與改寫
react搭建及threejs引入可以參考我的之前的博客,這里不多贅述
Three.js機器人與星系動態(tài)場景:實現(xiàn)3D渲染與交互式控制-CSDN博客
?引入必要信息
import { useEffect, useRef } from "react";
import * as THREE from "three";
import Stats from "three/examples/jsm/libs/stats.module.js";
import { GLTF, GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
初始化Render渲染器/Scene場景/?camer相機/controls軌道控制器
// 初始化渲染器的函數(shù)
/*** 初始化 WebGL 渲染器* @returns {THREE.WebGLRenderer} 創(chuàng)建并配置好的渲染器實例*/
// 初始化渲染
function initRender(): THREE.WebGLRenderer {// 創(chuàng)建一個WebGL渲染器const renderer = new THREE.WebGLRenderer({ antialias: true });// 根據(jù)設(shè)備像素比設(shè)置渲染器像素比renderer.setPixelRatio(window.devicePixelRatio);// 設(shè)置渲染器大小renderer.setSize(window.innerWidth, window.innerHeight);return renderer;
}// 初始化場景的函數(shù)
/*** 初始化場景* @param {THREE.WebGLRenderer} renderer - 渲染器實例* @returns {THREE.Scene} 創(chuàng)建并配置好的場景實例*/
function initScene(renderer: THREE.WebGLRenderer) {// 創(chuàng)建 PMREM 生成器const pmremGenerator = new THREE.PMREMGenerator(renderer);// 創(chuàng)建場景const scene = new THREE.Scene();// 設(shè)置場景背景scene.background = new THREE.Color(0xbfe3dd);// 設(shè)置場景環(huán)境scene.environment = pmremGenerator.fromScene(new RoomEnvironment(renderer), 0.04).texture;return scene;
}// 初始化相機的函數(shù)
/*** 初始化相機* @param {number} x - 相機在 x 軸的位置* @param {number} y - 相機在 y 軸的位置* @param {number} z - 相機在 z 軸的位置* @returns {THREE.PerspectiveCamera} 創(chuàng)建并配置好位置的相機實例*/
function initCamera(x: number, y: number, z: number) {const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 100);camera.position.set(x, y, z);return camera;
}// 初始化控制器的函數(shù)
/*** 初始化軌道控制器* @param {THREE.PerspectiveCamera} camera - 相機實例* @param {THREE.WebGLRenderer} renderer - 渲染器實例* @returns {OrbitControls} 創(chuàng)建并配置好的軌道控制器實例*/
function initControls(camera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer) {const controls = new OrbitControls(camera, renderer.domElement);controls.update();controls.enablePan = false;controls.enableDamping = true;return controls;
}
?組件核心方法Keyframes
采用react的函數(shù)式組件寫法,首字母大寫作為組件名,并導(dǎo)出
整個流程是初始化渲染器、scene場景、camera相機、controls軌道控制器;在場景中引入模型,并使用dracoLoader解壓GLTFLoader引入的模型,開啟模型上的動畫,設(shè)置場景動畫。
/*** Keyframes 組件函數(shù)*/
function Keyframes() {const containerRef = useRef<HTMLDivElement>(null); // 創(chuàng)建用于引用 HTML 元素的 refconst clock = new THREE.Clock(); // 創(chuàng)建時鐘實例const statsRef = useRef<Stats>(); // 創(chuàng)建用于引用統(tǒng)計信息的 refconst mixerRef = useRef<THREE.AnimationMixer>(); // 創(chuàng)建用于引用動畫混合器的 refconst renderer = initRender(); // 初始化渲染器const scene = initScene(renderer); // 初始化場景const camera = initCamera(5, 2, 10); // 初始化相機const controls = initControls(camera, renderer); // 初始化控制器controls.target.set(0, 0.5, 0); // 設(shè)置控制器的目標(biāo)const dracoLoader = new DRACOLoader(); // 創(chuàng)建 Draco 加載器dracoLoader.setDecoderPath("jsm/libs/draco/gltf/"); // 設(shè)置 Draco 解碼器路徑const loader = new GLTFLoader(); // 創(chuàng)建 GLTF 加載器loader.setDRACOLoader(dracoLoader); // 為 GLTF 加載器設(shè)置 Draco 加載器// 加載 GLTF 模型loader.load("models/gltf/LittlestTokyo.glb",(gltf: GLTF) => {const model = gltf.scene; // 獲取模型的場景model.position.set(1, 1, 0); // 設(shè)置模型的位置model.scale.set(0.01, 0.01, 0.01); // 設(shè)置模型的縮放scene.add(model); // 將模型添加到場景mixerRef.current = new THREE.AnimationMixer(model); // 創(chuàng)建動畫混合器mixerRef.current.clipAction(gltf.animations[0]).play(); // 播放動畫renderer.setAnimationLoop(animate); // 設(shè)置渲染循環(huán)},undefined,(e) => {console.error(e); // 處理加載錯誤},);// 渲染循環(huán)函數(shù)/*** 每一幀的更新和渲染邏輯*/function animate() {const delta = clock.getDelta(); // 獲取時間間隔mixerRef.current && mixerRef.current.update(delta); // 更新動畫混合器controls.update(); // 更新控制器statsRef.current && statsRef.current.update(); // 更新統(tǒng)計信息renderer.render(scene, camera); // 渲染場景和相機}// 處理窗口大小改變的函數(shù)/*** 處理窗口大小改變時的相機和渲染器更新*/function onWindowResize() {camera.aspect = window.innerWidth / window.innerHeight; // 更新相機的寬高比camera.updateProjectionMatrix(); // 更新相機的投影矩陣controls.update(); // 更新控制器renderer.setSize(window.innerWidth, window.innerHeight); // 更新渲染器的大小}// 使用 useEffect 鉤子useEffect(() => {if (!containerRef.current) return;containerRef.current.appendChild(renderer.domElement); // 將渲染器的 DOM 元素添加到引用的元素中statsRef.current = new Stats(); // 創(chuàng)建統(tǒng)計信息實例containerRef.current.appendChild(statsRef.current.dom); // 將統(tǒng)計信息的 DOM 元素添加到引用的元素中window.addEventListener("resize", onWindowResize); // 添加窗口大小改變的監(jiān)聽事件return () => {window.removeEventListener("resize", onWindowResize); // 清除窗口大小改變的監(jiān)聽事件renderer.setAnimationLoop(null); // 清除渲染循環(huán)};}, []);return <div ref={containerRef}></div>; // 返回一個帶有 ref 的 div 元素
}
export default Keyframes; // 導(dǎo)出 Keyframes 組件
通過<div ref={containerRef}></div>?創(chuàng)建一個dom元素,用于3D場景掛載
?模型加載與Draco解碼
示例模型提供的是壓縮后的模型,在頁面加載時需要進行解壓,必須使用dracoLoader方法,設(shè)置解碼方法所在路徑。在通過GLTFLoader導(dǎo)入。示例如下:
const dracoLoader = new DRACOLoader(); // 創(chuàng)建 Draco 加載器dracoLoader.setDecoderPath("jsm/libs/draco/gltf/"); // 設(shè)置 Draco 解碼器路徑const loader = new GLTFLoader(); // 創(chuàng)建 GLTF 加載器loader.setDRACOLoader(dracoLoader); // 為 GLTF 加載器設(shè)置 Draco 加載器// 加載 GLTF 模型loader.load("models/gltf/LittlestTokyo.glb",(gltf: GLTF) => {//處理模型},undefined,(e) => {console.error(e); // 處理加載錯誤},);
AnimationMixer 動畫混合器?
AnimationMixer動畫混合器是用于場景中特定對象的動畫的播放器。當(dāng)場景中的多個對象獨立動畫時,每個對象都可以使用同一個動畫混合器。
- 參數(shù):
rootObject
混合器播放的動畫所屬的對象。就是包含動畫模型的場景對象。- 常用參數(shù)和屬性:
.time
全局的混合器時間。.clipAction(AnimationClip)
返回所傳入的剪輯參數(shù)的AnimationAction
對象。AnimationAction
用來調(diào)度存儲在AnimationClip
中的動畫。
AnimationClip
動畫剪輯,是一個可重用的關(guān)鍵幀軌道集,它代表動畫。
.getRoot()
返回混合器的根對象。.update()
推進混合器時間并更新動畫。在渲染函數(shù)中調(diào)用更新動畫。
?在我們的示例中模型加載到場景時默認時沒有動畫的,也就是模型自身的動畫比如小火車和風(fēng)扇小人都是不動的。
?在模型加載的時候通過AnimationMixer開啟模型動畫
// 加載 GLTF 模型loader.load("models/gltf/LittlestTokyo.glb",(gltf: GLTF) => {const model = gltf.scene; // 獲取模型的場景model.position.set(1, 1, 0); // 設(shè)置模型的位置model.scale.set(0.01, 0.01, 0.01); // 設(shè)置模型的縮放scene.add(model); // 將模型添加到場景mixerRef.current = new THREE.AnimationMixer(model); // 創(chuàng)建動畫混合器mixerRef.current.clipAction(gltf.animations[0]).play(); // 播放動畫renderer.setAnimationLoop(animate); // 設(shè)置渲染循環(huán)},undefined,(e) => {console.error(e); // 處理加載錯誤},);
setAnimationLoop動畫循環(huán)
??
在Three.js中,
setAnimationLoop
?方法是用來設(shè)置一個函數(shù),這個函數(shù)會在每一幀被調(diào)用來進行渲染。這是必須的,因為在Three.js中,渲染循環(huán)不是自動開始的,你需要告訴渲染器何時以及如何進行渲染。
以下是為什么加載模型時必須使用?setAnimationLoop
?的一些原因:
-
渲染控制:通過?
setAnimationLoop
,你可以控制渲染循環(huán)的開始和結(jié)束。如果你不設(shè)置它,即使模型加載完成,也不會自動開始渲染過程。 -
動畫播放:在你的代碼中,你使用了?
AnimationMixer
?來播放模型中的動畫。這個動畫需要在每一幀更新,以確保動畫的連貫性和流暢性。setAnimationLoop
?允許你在每一幀更新動畫狀態(tài)。 -
性能優(yōu)化:使用?
setAnimationLoop
?可以讓你在不需要渲染的時候停止渲染,比如在瀏覽器標(biāo)簽頁不可見時,這樣可以節(jié)省資源并提高性能。 -
邏輯更新:在?
animate
?函數(shù)中,你可以執(zhí)行除了渲染之外的其他邏輯,比如更新動畫、控制器和統(tǒng)計信息等。這些更新是渲染過程的一部分,需要在每一幀進行。
如果你不使用?
setAnimationLoop
,你需要自己手動創(chuàng)建一個循環(huán)來不斷調(diào)用?renderer.render(scene, camera)
,并且確保在合適的時機更新動畫和其他邏輯。這通常是通過?requestAnimationFrame
?函數(shù)來實現(xiàn)的,但Three.js提供了?setAnimationLoop
?來簡化這一過程。總之,
setAnimationLoop
?是Three.js中用來啟動和維持渲染循環(huán)的關(guān)鍵方法,特別是在涉及到動畫的情況下,它是必須的。
?可以看到模型自身的多個動畫都動起來了
Stats.js幀檢測工具?
不管是做游戲還是做普通網(wǎng)頁,在這個時代基本都離不開動畫。說到動畫,第一個聯(lián)想到的概念就是“幀”。這是用來衡量和描述動畫是否流暢的一個單位。
示例程序的左上角有個工具窗口持續(xù)監(jiān)測FPS數(shù)值?
FPS是“Frames Per Second”的縮寫,意為“每秒幀數(shù)”。在視頻游戲和計算機圖形學(xué)中,FPS用來衡量顯示設(shè)備每秒鐘能夠顯示的靜止圖像(幀)的數(shù)量。這個數(shù)值越高,表示圖像更新得越快,視覺效果就越流暢。
在游戲領(lǐng)域,高FPS通常意味著更平滑的游戲體驗,尤其是在快速移動或復(fù)雜場景中。然而,FPS并不是唯一影響游戲體驗的因素,圖像質(zhì)量、響應(yīng)時間和系統(tǒng)穩(wěn)定性也同樣重要。
一般來說,人眼能夠感知到的流暢動畫大約需要30FPS以上,而60FPS或更高則被認為是高質(zhì)量游戲體驗的標(biāo)準(zhǔn)。不過,這也取決于個人的視覺感知能力和對流暢度的要求。
用法?
在使用?
npm install three?
下載的依賴包中已經(jīng)包含了?Stats.js
?了
可以這樣引入到項目中
import Stats from "three/examples/jsm/libs/stats.module.js";
通過new Stats()方法創(chuàng)建一個stats實例?。默認showPanel是0,顯示FPS面板。
?通過showPanel方法切換顯示方式;可以根據(jù)dom改變stats面板的位置,使用示例如下
const statsRef = useRef<Stats>(); // 創(chuàng)建用于引用統(tǒng)計信息的 refstatsRef.current = new Stats(); // 創(chuàng)建統(tǒng)計信息實例statsRef.current.showPanel(1);statsRef.current.dom.style.position = "absolute"; // 設(shè)置統(tǒng)計信息的 DOM 元素的位置statsRef.current.dom.style.top = "0px"; // 設(shè)置統(tǒng)計信息的 DOM 元素的位置statsRef.current.dom.style.left = "0px"; // 設(shè)置統(tǒng)計信息的 DOM 元素的位置
通過操作dom的方式將stats節(jié)點追加到3D場景中
containerRef.current.appendChild(statsRef.current.dom); // 將統(tǒng)計信息的 DOM 元素添加到引用的元素中
?默認就顯示在屏幕的左上角
當(dāng)點擊該面板時還可以切換監(jiān)聽的類型
?響應(yīng)式窗口
頁面加載時給了初始的renderer的寬高,但是如果用戶使用過程中可視區(qū)域發(fā)生了變化renderer無法自動使用屏幕
?可以在useEffect里通過事件監(jiān)聽瀏覽器的resize事件,當(dāng)瀏覽器尺寸變化時重新以最新的寬高設(shè)為renderer的尺寸信息
// 處理窗口大小改變的函數(shù)/*** 處理窗口大小改變時的相機和渲染器更新*/function onWindowResize() {camera.aspect = window.innerWidth / window.innerHeight; // 更新相機的寬高比camera.updateProjectionMatrix(); // 更新相機的投影矩陣controls.update(); // 更新控制器renderer.setSize(window.innerWidth, window.innerHeight); // 更新渲染器的大小}// 使用 useEffect 鉤子useEffect(() => {if (!containerRef.current) return;containerRef.current.appendChild(renderer.domElement); // 將渲染器的 DOM 元素添加到引用的元素中statsRef.current = new Stats(); // 創(chuàng)建統(tǒng)計信息實例containerRef.current.appendChild(statsRef.current.dom); // 將統(tǒng)計信息的 DOM 元素添加到引用的元素中window.addEventListener("resize", onWindowResize); // 添加窗口大小改變的監(jiān)聽事件return () => {window.removeEventListener("resize", onWindowResize); // 清除窗口大小改變的監(jiān)聽事件renderer.setAnimationLoop(null); // 清除渲染循環(huán)};}, []);
完整代碼?
import { useEffect, useRef } from "react";
import * as THREE from "three";
import Stats from "three/examples/jsm/libs/stats.module.js";
import { GLTF, GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";// 初始化渲染器的函數(shù)
/*** 初始化 WebGL 渲染器* @returns {THREE.WebGLRenderer} 創(chuàng)建并配置好的渲染器實例*/
// 初始化渲染
function initRender(): THREE.WebGLRenderer {// 創(chuàng)建一個WebGL渲染器const renderer = new THREE.WebGLRenderer({ antialias: true });// 根據(jù)設(shè)備像素比設(shè)置渲染器像素比renderer.setPixelRatio(window.devicePixelRatio);// 設(shè)置渲染器大小renderer.setSize(window.innerWidth, window.innerHeight);return renderer;
}// 初始化場景的函數(shù)
/*** 初始化場景* @param {THREE.WebGLRenderer} renderer - 渲染器實例* @returns {THREE.Scene} 創(chuàng)建并配置好的場景實例*/
function initScene(renderer: THREE.WebGLRenderer) {// 創(chuàng)建 PMREM 生成器const pmremGenerator = new THREE.PMREMGenerator(renderer);// 創(chuàng)建場景const scene = new THREE.Scene();// 設(shè)置場景背景scene.background = new THREE.Color(0xbfe3dd);// 設(shè)置場景環(huán)境scene.environment = pmremGenerator.fromScene(new RoomEnvironment(renderer), 0.04).texture;return scene;
}// 初始化相機的函數(shù)
/*** 初始化相機* @param {number} x - 相機在 x 軸的位置* @param {number} y - 相機在 y 軸的位置* @param {number} z - 相機在 z 軸的位置* @returns {THREE.PerspectiveCamera} 創(chuàng)建并配置好位置的相機實例*/
function initCamera(x: number, y: number, z: number) {const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 100);camera.position.set(x, y, z);return camera;
}// 初始化控制器的函數(shù)
/*** 初始化軌道控制器* @param {THREE.PerspectiveCamera} camera - 相機實例* @param {THREE.WebGLRenderer} renderer - 渲染器實例* @returns {OrbitControls} 創(chuàng)建并配置好的軌道控制器實例*/
function initControls(camera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer) {const controls = new OrbitControls(camera, renderer.domElement);controls.update();controls.enablePan = false;controls.enableDamping = true;return controls;
}/*** Keyframes 組件函數(shù)*/
function Keyframes() {const containerRef = useRef<HTMLDivElement>(null); // 創(chuàng)建用于引用 HTML 元素的 refconst clock = new THREE.Clock(); // 創(chuàng)建時鐘實例const statsRef = useRef<Stats>(); // 創(chuàng)建用于引用統(tǒng)計信息的 refconst mixerRef = useRef<THREE.AnimationMixer>(); // 創(chuàng)建用于引用動畫混合器的 refconst renderer = initRender(); // 初始化渲染器const scene = initScene(renderer); // 初始化場景const camera = initCamera(5, 2, 10); // 初始化相機const controls = initControls(camera, renderer); // 初始化控制器controls.target.set(0, 0.5, 0); // 設(shè)置控制器的目標(biāo)const dracoLoader = new DRACOLoader(); // 創(chuàng)建 Draco 加載器dracoLoader.setDecoderPath("jsm/libs/draco/gltf/"); // 設(shè)置 Draco 解碼器路徑const loader = new GLTFLoader(); // 創(chuàng)建 GLTF 加載器loader.setDRACOLoader(dracoLoader); // 為 GLTF 加載器設(shè)置 Draco 加載器// 加載 GLTF 模型loader.load("models/gltf/LittlestTokyo.glb",(gltf: GLTF) => {const model = gltf.scene; // 獲取模型的場景model.position.set(1, 1, 0); // 設(shè)置模型的位置model.scale.set(0.01, 0.01, 0.01); // 設(shè)置模型的縮放scene.add(model); // 將模型添加到場景mixerRef.current = new THREE.AnimationMixer(model); // 創(chuàng)建動畫混合器mixerRef.current.clipAction(gltf.animations[0]).play(); // 播放動畫renderer.setAnimationLoop(animate); // 設(shè)置渲染循環(huán)},undefined,(e) => {console.error(e); // 處理加載錯誤},);// 渲染循環(huán)函數(shù)/*** 每一幀的更新和渲染邏輯*/function animate() {const delta = clock.getDelta(); // 獲取時間間隔mixerRef.current && mixerRef.current.update(delta); // 更新動畫混合器controls.update(); // 更新控制器statsRef.current && statsRef.current.update(); // 更新統(tǒng)計信息renderer.render(scene, camera); // 渲染場景和相機}// 處理窗口大小改變的函數(shù)/*** 處理窗口大小改變時的相機和渲染器更新*/function onWindowResize() {camera.aspect = window.innerWidth / window.innerHeight; // 更新相機的寬高比camera.updateProjectionMatrix(); // 更新相機的投影矩陣controls.update(); // 更新控制器renderer.setSize(window.innerWidth, window.innerHeight); // 更新渲染器的大小}// 使用 useEffect 鉤子useEffect(() => {if (!containerRef.current) return;containerRef.current.appendChild(renderer.domElement); // 將渲染器的 DOM 元素添加到引用的元素中statsRef.current = new Stats(); // 創(chuàng)建統(tǒng)計信息實例containerRef.current.appendChild(statsRef.current.dom); // 將統(tǒng)計信息的 DOM 元素添加到引用的元素中window.addEventListener("resize", onWindowResize); // 添加窗口大小改變的監(jiān)聽事件return () => {window.removeEventListener("resize", onWindowResize); // 清除窗口大小改變的監(jiān)聽事件renderer.setAnimationLoop(null); // 清除渲染循環(huán)};}, []);return <div ref={containerRef}></div>; // 返回一個帶有 ref 的 div 元素
}export default Keyframes; // 導(dǎo)出 Keyframes 組件