Three.js 打造繽紛夏日3D夢中情島 🌊
- 2022 年 5 月 27 日
- 筆記
聲明:本文涉及圖文和模型素材僅用於個人學習、研究和欣賞,請勿二次修改、非法傳播、轉載、出版、商用、及進行其他獲利行為。
背景
深居內陸的人們,大概每個人都有過大海之夢吧。夏日傍晚在沙灘漫步奔跑;或是在海上衝浪游泳;或是在海島遊玩探險;亦或靜待日出日落……本文使用 React + Three.js
技術棧,實現 3D
海洋和島嶼,主要包含知識點包括:Tone Mapping
、Water
類、Sky
類、Shader
著色、ShaderMaterial
著色器材質、Raycaster
檢測遮擋以及 Three.js
的其他基礎知識,讓我們在這個夏天通過此頁面共赴大海之約。
效果
💻
本頁面僅適配PC
端,大屏訪問效果更佳。👁🗨
在線預覽地址1://3d-eosin.vercel.app/#/ocean👁🗨
在線預覽地址2://dragonir.github.io/3d/#/ocean
實現
👨🎨 素材準備
開發之前,需要準備頁面所需的素材,本文用到的海島素材是在 sketchfab.com 找的免費模型。下載好素材之後,在 Blender
中打開,按自己的想法調整模型的顏色、材質、大小比例、角度、位置等資訊,刪減不需要的模組、縮減面數以壓縮模型體積,最後刪除相機、光照、UV
、動畫等多餘資訊,只導出模型網格備用。
📦 資源引入
首先,引入開發所需的必備資源,OrbitControls
用於鏡頭軌道控制;GLTFLoader
用於載入 gltf
格式模型;Water
是 Three.js
內置的一個類,可以生成類似水的效果;Sky
可以生成天空效果;TWEEN
用來生成補間動畫;Animations
是對 TWEEN
控制鏡頭補間動畫方法的封裝;waterTexture
、flamingoModel
、islandModel
三者分別是水的法向貼圖、飛鳥模型、海島模型;vertexShader
和 fragmentShader
是用於生成彩虹的 Shader
著色器。
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { Water } from 'three/examples/jsm/objects/Water';
import { Sky } from 'three/examples/jsm/objects/Sky';
import { TWEEN } from "three/examples/jsm/libs/tween.module.min";
import Animations from '@/assets/utils/animations';
import waterTexture from '@/containers/Ocean/images/waternormals.jpg';
import islandModel from '@/containers/Ocean/models/island.glb';
import flamingoModel from '@/containers/Ocean/models/flamingo.glb';
import vertexShader from '@/containers/Ocean/shaders/rainbow/vertex.glsl';
import fragmentShader from '@/containers/Ocean/shaders/rainbow/fragment.glsl';
📃 頁面結構
頁面主要由3部分構成:canvas.webgl
用於渲染 WEBGL
場景;div.loading
用於模型載入完成前顯示載入進度;div.point
用於添加交互點,省略部分是其他幾個交互點資訊。
render () {
return (
<div className='ocean'>
<canvas className='webgl'></canvas>
{this.state.loadingProcess === 100 ? '' : (
<div className='loading'>
<span className='progress'>{this.state.loadingProcess} %</span>
</div>
)}
<div className="point point-0">
<div className="label label-0">1</div>
<div className="text">燈塔:矗立在海岸的岩石之上,白色的塔身以及紅色的塔屋,在湛藍色的天空和深藍色大海的映襯下,顯得如此醒目和美麗。</div>
</div>
// ...
</div>
)
}
🌏 場景初始化
在這部分,先定義好需要的狀態值,loadingProcess
用於顯示頁面載入進度。
state = {
loadingProcess: 0
}
定義一些全局變數和參數,初始化場景、相機、鏡頭軌道控制器、燈光、頁面縮放監聽等。
const clock = new THREE.Clock();
const raycaster = new THREE.Raycaster()
const sizes = {
width: window.innerWidth,
height: window.innerHeight
}
const renderer = new THREE.WebGLRenderer({
canvas: document.querySelector('canvas.webgl'),
antialias: true
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.setSize(sizes.width, sizes.height);
// 設置渲染效果
renderer.toneMapping = THREE.ACESFilmicToneMapping;
// 創建場景
const scene = new THREE.Scene();
// 創建相機
const camera = new THREE.PerspectiveCamera(55, sizes.width / sizes.height, 1, 20000);
camera.position.set(0, 600, 1600);
// 添加鏡頭軌道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.enableDamping = true;
controls.enablePan = false;
controls.maxPolarAngle = 1.5;
controls.minDistance = 50;
controls.maxDistance = 1200;
// 添加環境光
const ambientLight = new THREE.AmbientLight(0xffffff, .8);
scene.add(ambientLight);
// 添加平行光
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.color.setHSL(.1, 1, .95);
dirLight.position.set(-1, 1.75, 1);
dirLight.position.multiplyScalar(30);
scene.add(dirLight);
// 頁面縮放監聽並重新更新場景和相機
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}, false);
💡
Tone Mapping
可以注意到,本文使用了 renderer.toneMapping = THREE.ACESFilmicToneMapping
來設置頁面渲染效果。目前 Three.js
中有以下幾種 Tone Mapping
值,它們定義了 WebGLRenderer
的 toneMapping
屬性,用於在近似標準電腦顯示器或移動設備的低動態範圍 LDR
螢幕上展示高動態範圍 HDR
外觀。大家可以修改不同的值看看渲染效果有何不同。
THREE.NoToneMapping
THREE.LinearToneMapping
THREE.ReinhardToneMapping
THREE.CineonToneMapping
THREE.ACESFilmicToneMapping
🌊 海
使用 Three.js
自帶的 Water
類創建海洋,首先創建一個平面網格 waterGeometry
,讓後將它傳遞給 Water
,並配置相關屬性,最後將海洋添加到場景中。
const waterGeometry = new THREE.PlaneGeometry(10000, 10000);
const water = new Water(waterGeometry, {
textureWidth: 512,
textureHeight: 512,
waterNormals: new THREE.TextureLoader().load(waterTexture, texture => {
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
}),
sunDirection: new THREE.Vector3(),
sunColor: 0xffffff,
waterColor: 0x0072ff,
distortionScale: 4,
fog: scene.fog !== undefined
});
water.rotation.x = - Math.PI / 2;
scene.add(water);
💡
Water 類
參數說明:
textureWidth
:畫布寬度textureHeight
:畫布高度waterNormals
:法向量貼圖sunDirection
:陽光方向sunColor
:陽光顏色waterColor
:水顏色distortionScale
:物體倒影分散度fog
:霧alpha
:透明度
🌞 空
接著,使用 Three.js
自帶的天空類 Sky
創建天空,通過修改著色器參數設置天空樣式,然後創建太陽並添加到場景中。
const sky = new Sky();
sky.scale.setScalar(10000);
scene.add(sky);
const skyUniforms = sky.material.uniforms;
skyUniforms['turbidity'].value = 20;
skyUniforms['rayleigh'].value = 2;
skyUniforms['mieCoefficient'].value = 0.005;
skyUniforms['mieDirectionalG'].value = 0.8;
// 太陽
const sun = new THREE.Vector3();
const pmremGenerator = new THREE.PMREMGenerator(renderer);
const phi = THREE.MathUtils.degToRad(88);
const theta = THREE.MathUtils.degToRad(180);
sun.setFromSphericalCoords(1, phi, theta);
sky.material.uniforms['sunPosition'].value.copy(sun);
water.material.uniforms['sunDirection'].value.copy(sun).normalize();
scene.environment = pmremGenerator.fromScene(sky).texture;
💡
Sky 類
天空材質著色器參數說明:
turbidity
渾濁度rayleigh
視覺效果就是傍晚晚霞的紅光的深度luminance
視覺效果整體提亮或變暗mieCoefficient
散射係數mieDirectionalG
定向散射值
🌈 虹
首先,創建具有彩虹漸變效果的著色器 Shader
, 然後使用著色器材質 ShaderMaterial
, 創建圓環 THREE.TorusGeometry
並添加到場景中。
頂點著色器 vertex.glsl:
varying vec2 vUV;
varying vec3 vNormal;
void main () {
vUV = uv;
vNormal = vec3(normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
片段著色器 fragment.glsl:
varying vec2 vUV;
varying vec3 vNormal;
void main () {
vec4 c = vec4(abs(vNormal) + vec3(vUV, 0.0), 0.1); // 設置透明度為0.1
gl_FragColor = c;
}
彩虹漸變著色器效果:
const material = new THREE.ShaderMaterial({
side: THREE.DoubleSide,
transparent: true,
uniforms: {},
vertexShader: vertexShader,
fragmentShader: fragmentShader
});
const geometry = new THREE.TorusGeometry(200, 10, 50, 100);
const torus = new THREE.Mesh(geometry, material);
torus.opacity = .1;
torus.position.set(0, -50, -400);
scene.add(torus);
💡
Shader 著色器
WebGL
中記述了坐標變換的機制就叫做著色器 Shader
,著色器又有處理幾何圖形頂點的 頂點著色器
和處理像素的 片段著色器
兩種類型
準備頂點著色器和片元著色器
著色器的添加有多種方法,最簡單的方法就是把著色器記錄在 HTML
中。該方法利用HTML
的 script
標籤來實現,如:
頂點著色器:
<script id="vshader" type="x-shader/x-vertex"></script>
片段著色器:
<script id="fshader" type="x-shader/x-fragment"></script>
🎏
也可以像本文中一樣,直接使用單獨創建glsl
格式文件引入。
著色器的三個變數與運行方式
Uniforms
:是所有頂點都具有相同的值的變數。 比如燈光,霧,和陰影貼圖就是被儲存在uniforms
中的數據。uniforms
可以通過頂點著色器和片元著色器來訪問。Attributes
:是與每個頂點關聯的變數。例如,頂點位置,法線和頂點顏色都是存儲在attributes
中的數據。attributes
只可以在頂點著色器中訪問。Varyings
:是從頂點著色器傳遞到片元著色器的變數。對於每一個片元,每一個varying
的值將是相鄰頂點值的平滑插值。
頂點著色器
首先運行,它接收 attributes
, 計算每個單獨頂點的位置,並將其他數據varyings
傳遞給片段著色器。片段著色器
後運行,它設置渲染到螢幕的每個單獨的片段的顏色。
💡
ShaderMaterial 著色器材質
Three.js
所謂的材質對象 Material
本質上就是著色器程式碼和需要傳遞的 uniform
數據光源、顏色、矩陣。Three.js
提供可直接渲染著色器語法的材質 ShaderMaterial
和 RawShaderMaterial
。
RawShaderMaterial
: 和原生WebGL
中一樣,頂點著色器、片元著色器程式碼基本沒有任何區別,不過頂點數據和uniform
數據可以通過Three.js
的API
快速傳遞,要比使用WebGL
原生的API
與著色器變數綁定要方便得多。ShaderMaterial
:ShaderMaterial
比RawShaderMaterial
更方便些,著色器中的很多變數不用聲明,Three.js
系統會自動設置,比如頂點坐標變數、投影矩陣、視圖矩陣等。
構造函數:
ShaderMaterial(parameters : Object)
parameters
:可選,用於定義材質外觀的對象,具有一個或多個屬性。
常用屬性:
attributes[Object]
:接受如下形式的對象,{ attribute1: { value: []} }
指定要傳遞給頂點著色器程式碼的attributes
;鍵為attribute
修飾變數的名稱,值也是對象格式,如{ value: [] }
,value
是固定名稱,因為attribute
相對於所有頂點,所以應該回傳一個數組格式。只有bufferGeometry
類型的能使用該屬性。.uniforms[Object]
:如下形式的對象:{ uniform1: { value: 1.0 }, uniform2: { value: 2.0 }}
指定要傳遞給shader
程式碼的uniforms
;鍵為uniform
的名稱,值是如下形式:{ value: 1.0 }
這裡value
是uniform
的值。名稱必須匹配著色器程式碼中uniform
的name
,和GLSL
程式碼中的定義一樣。 注意,uniforms
逐幀被刷新,所以更新uniform
值將立即更新GLSL
程式碼中的相應值。.fragmentShader[String]
:片元著色器的GLSL
程式碼,它也可以作為一個字元串直接傳遞或者通過AJAX
載入。.vertexShader[String]
:頂點著色器的GLSL
程式碼,它也可以作為一個字元串直接傳遞或者通過AJAX
載入。
🌴 島
接著,使用 GLTFLoader
載入島嶼模型並添加到場景中。載入之前可以使用 LoadingManager
來管理載入進度。
const manager = new THREE.LoadingManager();
manager.onProgress = async(url, loaded, total) => {
if (Math.floor(loaded / total * 100) === 100) {
this.setState({ loadingProcess: Math.floor(loaded / total * 100) });
Animations.animateCamera(camera, controls, { x: 0, y: 40, z: 140 }, { x: 0, y: 0, z: 0 }, 4000, () => {
this.setState({ sceneReady: true });
});
} else {
this.setState({ loadingProcess: Math.floor(loaded / total * 100) });
}
};
const loader = new GLTFLoader(manager);
loader.load(islandModel, mesh => {
mesh.scene.traverse(child => {
if (child.isMesh) {
child.material.metalness = .4;
child.material.roughness = .6;
}
})
mesh.scene.position.set(0, -2, 0);
mesh.scene.scale.set(33, 33, 33);
scene.add(mesh.scene);
});
🦅 鳥
使用 GLTFLoader
載入島嶼模型添加到場景中,獲取模型自帶的動畫幀並進行播放,記得要在 requestAnimationFrame
中更新動畫。可以使用 clone
方法在場景中添加多隻飛鳥。鳥模型來源於 Three.js
官網。
loader.load(flamingoModel, gltf => {
const mesh = gltf.scene.children[0];
mesh.scale.set(.35, .35, .35);
mesh.position.set(-100, 80, -300);
mesh.rotation.y = - 1;
mesh.castShadow = true;
scene.add(mesh);
const mixer = new THREE.AnimationMixer(mesh);
mixer.clipAction(gltf.animations[0]).setDuration(1.2).play();
this.mixers.push(mixer);
});
🖐 交互點
添加交互點,滑鼠 hover
懸浮時顯示提示語,點擊交互點可以切換鏡頭角度,視角聚焦到交互點對應的位置 📍
上。
const points = [
{
position: new THREE.Vector3(10, 46, 0),
element: document.querySelector('.point-0')
},
// ...
];
document.querySelectorAll('.point').forEach(item => {
item.addEventListener('click', event => {
let className = event.target.classList[event.target.classList.length - 1];
switch(className) {
case 'label-0':
Animations.animateCamera(camera, controls, { x: -15, y: 80, z: 60 }, { x: 0, y: 0, z: 0 }, 1600, () => {});
break;
// ...
}
}, false);
});
🎥 動畫
在 requestAnimationFrame
中更新水、鏡頭軌道控制器、相機、TWEEN
、交互點等動畫。
const animate = () => {
requestAnimationFrame(animate);
water.material.uniforms['time'].value += 1.0 / 60.0;
controls && controls.update();
const delta = clock.getDelta();
this.mixers && this.mixers.forEach(item => {
item.update(delta);
});
const timer = Date.now() * 0.0005;
TWEEN && TWEEN.update();
camera && (camera.position.y += Math.sin(timer) * .05);
if (this.state.sceneReady) {
// 遍歷每個點
for (const point of points) {
// 獲取2D螢幕位置
const screenPosition = point.position.clone();
screenPosition.project(camera);
raycaster.setFromCamera(screenPosition, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length === 0) {
// 未找到相交點,顯示
point.element.classList.add('visible');
} else {
// 找到相交點
// 獲取相交點的距離和點的距離
const intersectionDistance = intersects[0].distance;
const pointDistance = point.position.distanceTo(camera.position);
// 相交點距離比點距離近,隱藏;相交點距離比點距離遠,顯示
intersectionDistance < pointDistance ? point.element.classList.remove('visible') : point.element.classList.add('visible');
}
const translateX = screenPosition.x * sizes.width * 0.5;
const translateY = - screenPosition.y * sizes.height * 0.5;
point.element.style.transform = `translateX(${translateX}px) translateY(${translateY}px)`;
}
}
renderer.render(scene, camera);
}
animate();
}
💡
Raycaster 檢測遮擋
仔細觀察,在上述 👆
更新交互點動畫的方法中,通過 raycaster
射線來檢查交互點是否被物體遮擋,如果被遮擋就隱藏交互點,否則顯示交互點,大家可以通過旋轉場景觀察到這一效果。
總結
本文包含的新知識點主要包括:
Tone Mapping
Water
類Sky
類Shader
著色器ShaderMaterial
著色器材質Raycaster
檢測遮擋
想了解其他前端知識或其他未在本文中詳細描述的
Web 3D
開發技術相關知識,可閱讀我往期的文章。轉載請註明原文地址和作者。如果覺得文章對你有幫助,不要忘了一鍵三連哦 👍。
參考
- [1]. //threejs.org
附錄
-
...
-
[1]. 📷 前端實現很哇塞的瀏覽器端掃碼功能
-
[2]. 🌏 前端瓦片地圖載入之塞爾達傳說曠野之息
-
...
本文作者:dragonir 本文地址://www.cnblogs.com/dragonir/p/16316217.html