Plongez dans Lyon網(wǎng)站終于上線了。 我們與 Danka 團(tuán)隊(duì)和 Nico Icecream 共同努力,打造了一個(gè)令我們特別自豪的流暢的沉浸式網(wǎng)站。
這個(gè)網(wǎng)站是專為 ONLYON Tourism 和會(huì)議而建,旨在展示里昂最具標(biāo)志性的活動(dòng)場(chǎng)所。觀看簡(jiǎn)短的介紹視頻后,用戶可以進(jìn)入城市的交互式風(fēng)景如畫的地圖,所有場(chǎng)館都建模為 3D 對(duì)象。 每個(gè)建筑物都可以點(diǎn)擊,進(jìn)入一個(gè)詳細(xì)說明位置信息的專用頁面。
推薦:用 NSDT編輯器 快速搭建可編程3D場(chǎng)景。
1、打造沉浸式體驗(yàn)
主要網(wǎng)站導(dǎo)航體驗(yàn)依賴于卡通般的 WebGL 場(chǎng)景,其中包含大量景觀元素、云彩、動(dòng)畫車輛、波光粼粼的河流,當(dāng)然還有建筑物。
總而言之,它由 63 個(gè)幾何圖形、48 個(gè)紋理、32234 個(gè)三角形(以及一些后期處理魔法)組成。 當(dāng)你處理大量對(duì)象時(shí),必須組織代碼架構(gòu)并使用一些技巧來優(yōu)化性能。
2、3D場(chǎng)景
所有模型均由才華橫溢的 3D 藝術(shù)家 Nicolas Dufoure(又名 Icecream)在 3ds Max 中創(chuàng)建,然后使用 Blender 導(dǎo)出為 GTLF 對(duì)象。如果你有一些現(xiàn)成的3D模型可以利用,那么可以使用這個(gè)在線3D格式轉(zhuǎn)換工具將它們轉(zhuǎn)換成GLTF模型,這會(huì)節(jié)省不少時(shí)間。
2.1 藝術(shù)指導(dǎo)和視覺構(gòu)成
Nico 和 Danka 團(tuán)隊(duì)從地圖的早期迭代開始了項(xiàng)目的創(chuàng)作過程,并很快確定了低多邊形和豐富多彩的藝術(shù)方向。
與客戶品牌調(diào)色板相匹配的早期地圖迭代之一
我們知道必須添加兩打可點(diǎn)擊的建筑物,因此我們必須在視覺構(gòu)圖、導(dǎo)航便利性和性能之間找到適當(dāng)?shù)钠胶狻?/p>
左:第一個(gè)場(chǎng)景合成測(cè)試渲染,右:早期 webgl 壓力測(cè)試
為了將繪制的三角形數(shù)量保持在最低限度,我們還很快決定限制場(chǎng)景左側(cè)和右側(cè)遠(yuǎn)側(cè)的 3D 對(duì)象的數(shù)量。 但過了一段時(shí)間,我們意識(shí)到我們實(shí)際上必須阻止用戶看到這些區(qū)域。
這個(gè)地方看起來很空,不是嗎?
2.2 相機(jī)操作
為了避免平移、縮放和動(dòng)畫之間的任何沖突,我很早就決定從頭開始編寫相機(jī)控件的代碼。 事實(shí)證明這非常方便,因?yàn)橹鬄橄鄼C(jī)可能的位置添加閾值并不困難。
白色三角形代表我們實(shí)際的相機(jī)范圍
這樣,我們成功地限制了相機(jī)的移動(dòng),同時(shí)仍然允許用戶探索所有地圖重要區(qū)域。
2.3 烘焙和壓縮紋理
為了節(jié)省大量 GPU 工作負(fù)載,Nico 和我同意的另一件事是用全局照明和陰影烘焙所有紋理。
當(dāng)然,這意味著更多的建模工作,如果你的場(chǎng)景需要頻繁更改,這可能會(huì)很煩人。 但它減輕了 GPU 的大量計(jì)算負(fù)擔(dān)(光照陰影、陰影貼圖……),在我們的例子中,這絕對(duì)是值得的。
3D場(chǎng)景建模概述
當(dāng)處理如此數(shù)量的紋理(通常為 1024x1024、2048x2048 甚至 4096x4096 像素寬)時(shí),你應(yīng)該考慮的另一件事是使用基礎(chǔ)壓縮紋理。
如果你從未聽說過,基礎(chǔ)紋理基本上比 jpeg/png 紋理占用更少的 GPU 內(nèi)存。 當(dāng)它們從 CPU 上傳到 GPU 時(shí),它們還可以降低主線程瓶頸。
你可以在這里非常輕松地生成基礎(chǔ)紋理。
3、代碼架構(gòu)和組織
當(dāng)需要處理如此多的資源時(shí),組織代碼的最佳方法是創(chuàng)建幾個(gè) javascript 類(或函數(shù),當(dāng)然取決于你)并將它們組織在目錄和文件中。
通常,我是這樣組織該項(xiàng)目的文件和文件夾的:
webgl
|-- data
| |-- objects.js
| |-- otherObjects.js
|-- shaders
| |-- customShader.js
| |-- anotherShader.js
|-- CameraController.js
|-- GroupRaycaster.js
|-- ObjectsLoader.js
|-- WebGLExperience.js
- data文件夾包含單獨(dú)文件中的 javascript 對(duì)象以及所有信息
- shaders文件夾包含單獨(dú)文件中的所有項(xiàng)目自定義著色器
- CameraController.js:處理所有相機(jī)移動(dòng)和控制的類
- GroupRaycaster.js:處理所有“交互式”對(duì)象光線投射的類
- ObjectsLoader.js:加載所有場(chǎng)景對(duì)象的類
- WebGLExperience.js:初始化渲染器、相機(jī)、場(chǎng)景、后處理并處理所有其他類的主類
當(dāng)然,你可以自由地以不同的方式組織它。 例如,有些人喜歡為渲染器、場(chǎng)景和相機(jī)創(chuàng)建單獨(dú)的類。
3.1 核心的概念代碼摘錄
那么讓我們進(jìn)入代碼本身吧!
以下是一些文件實(shí)際外觀的詳細(xì)示例。
Obects.js :
import { customFragmentShader } from "../shaders/customShader";
const sceneObjects = [
{
subPath: "path/to/",
gltf: "object1.gltf"
},
{
subPath: "anotherPath/to/",
gltf: "object2.gltf",
fragmentShader: customFragmentShader,
uniforms: {
uTime: {
value: 0,
}
}
}
];
export default sceneObjects;
ObjectsLoader.js:
import { LoadingManager } from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { BasisTextureLoader } from "three/examples/jsm/loaders/BasisTextureLoader";
export default class ObjectsLoader {
constructor({
renderer, // our threejs renderer
basePath = '/', // common base path for all your assets
onLoading = () => {}, // onLoading callback
onComplete = () => {} // onComplete callback
}) {
this.renderer = renderer;
this.basePath = basePath;
this.loadingManager = new LoadingManager();
this.basisLoader = new BasisTextureLoader(this.loadingManager);
// you can also host those files locally if you want
this.basisLoader.setTranscoderPath("/node_modules/three/examples/js/libs/basis/");
this.basisLoader.detectSupport(this.renderer);
this.loadingManager.addHandler(/\.basis$/i, this.basisLoader);
this.loader = new GLTFLoader(this.loadingManager);
this.loader.setPath(this.basePath);
this.onLoading = onLoading;
this.onComplete = onComplete;
this.objects = [];
this.state = {
objectsLoaded: 0,
totalObjects: 0,
isComplete: false,
};
this.loadingManager.onProgress = (url, itemsLoaded, itemsTotal) => {
const percent = Math.ceil((itemsLoaded / itemsTotal) * 100);
// loading callback
this.onLoading && this.onLoading(percent);
if(percent === 100 && !this.state.isComplete) {
this.state.isComplete = true;
this.isLoadingComplete();
}
};
this.loadingManager.onError = (url) => {
console.warn('>>> error while loading: ', url);
};
}
loadObject({
object,
parent, // could be our main scene or a group
onSuccess = () => {} // callback for each object loaded if needed
}) {
if(!object || !object.gltf) return;
if('requestIdleCallback' in window) {
window.requestIdleCallback(() => {
this.startLoading({
object,
parent,
onSuccess
});
});
}
else {
this.startLoading({
object,
parent,
onSuccess
});
}
}
startLoading({
object,
parent,
onSuccess
}) {
this.state.totalObjects++;
// if object has a subpath
if(object.subPath) {
this.loader.setPath(this.basePath + object.subPath);
}
this.loader.load(object.gltf, (gltf) => {
const sceneObject = {
gltf,
};
// ... do whatever you want with your gltf scene here
// ... like using a ShaderMaterial if object.fragmentShader is defined for example!
parent.add(gltf.scene);
this.objects.push(sceneObject);
onSuccess && onSuccess(sceneObject);
// check if we've load everything
this.state.objectsLoaded++;
this.isLoadingComplete();
}, (xhr) => {
},(error) => {
console.warn( 'An error happened', error );
this.state.objectsLoaded++;
this.isLoadingComplete();
});
}
isLoadingComplete() {
if(this.state.isComplete && this.state.objectsLoaded === this.state.totalObjects) {
setTimeout(() => {
this.onComplete && this.onComplete();
}, 0);
}
}
}
WebGLExperience.js:
import {
WebGLRenderer,
Scene,
sRGBEncoding,
Group
} from "three";
import ObjectsLoader from "./ObjectsLoader";
import CameraController from "./CameraController";
import GroupRaycaster from "./GroupRaycaster";
import sceneObjects from "./data/objects";
/***
Project architecture example:
webgl
|-- data
| |-- objects.js
| |-- otherObjects.js
|-- shaders
| |-- customShader.js
| |-- anotherShader.js
|-- CameraController.js
|-- GroupRaycaster.js
|-- ObjectsLoader.js
|-- WebGLExperience.js
*/
export default class WebGLExperience {
constructor({
// add params here if needed
container = document.body,
}) {
this.container = container;
// update on resize
this.width = window.innerWidth;
this.height = window.innerHeight;
this.initRenderer();
this.initScene();
this.initCamera();
this.loadObjects();
this.initRaycasting();
}
/*** EVENTS CALLBACKS ***/
onLoading(callback) {
if(callback) {
this.onLoadingCallback = callback;
}
return this;
}
onComplete(callback) {
if(callback) {
this.onCompleteCallback = callback;
}
return this;
}
/*** THREEJS SETUP ***/
initRenderer() {
this.renderer = new WebGLRenderer({
antialias: true,
alpha: true,
});
// important when dealing with GLTFs!
this.renderer.outputEncoding = sRGBEncoding;
this.renderer.setSize( this.width, this.height );
this.renderer.setClearColor( 0xffffff, 1 );
this.renderer.outputEncoding = sRGBEncoding;
// append the canvas
this.container.appendChild( this.renderer.domElement );
}
initScene() {
// scene
this.scene = new Scene();
}
initCamera() {
// creates the camera and handles the controls & movements
this.cameraController = new CameraController({
webgl: this,
});
this.camera = this.cameraController.camera;
}
/*** RAYCASTING ***/
initRaycasting() {
this.raycaster = new GroupRaycaster({
camera: this.camera,
width: this.width,
height: this.height,
onMouseEnteredObject: (object) => {
// raycasted object mouse enter event
},
onMouseLeavedObject: (object) => {
// raycasted object mouse leave event
},
onObjectClicked: (object) => {
// raycasted object mouse click event
}
});
}
/*** LOAD OBJECTS ***/
loadObjects() {
this.objectsLoader = new ObjectsLoader({
renderer: this.renderer,
basePath: '/assets/', // whatever
onLoading: (percent) => {
console.log(percent);
// callback
this.onLoadingCallback && this.onLoadingCallback(percent);
},
onComplete: () => {
// loading complete...
console.log("loading complete!");
// callback
this.onCompleteCallback && this.onCompleteCallback();
}
});
// create a new group where we'll add all our objects
this.objectGroup = new Group();
this.scene.add(this.objectGroup);
// load the objects
sceneObjects.forEach(object => {
this.objectsLoader.loadObject({
object,
parent: this.objectGroup,
onSuccess: (loadedObject) => {
console.log(loadedObject);
}
});
});
}
/*** RENDERING ***/
// ...other methods to handle rendering, interactions, etc.
}
3.2 與 Nextjs / React 集成
由于該項(xiàng)目使用 Nextjs,我們需要在 React 組件內(nèi)實(shí)例化我們的 WebGLExperience 類。
我們只需創(chuàng)建一個(gè) WebGLCanvas 組件并將其放在路由器外部,以便它始終位于 DOM 中。
WebGLCanvas.jsx:
import React, {useRef, useState, useEffect} from 'react';
import WebGLExperience from '../../webgl/WebGLExperience';
import styles from './WebGLCanvas.module.scss';
export default function WebGLCanvas() {
const container = useRef();
const [ webglXP, setWebglXP ] = useState();
// set up webgl context on init
useEffect(() => {
const webgl = new WebGLExperience({
container: container.current,
});
setWebglXP(webgl);
}, []);
// now we can watch webglXP inside a useEffect hook
// and do what we want with it
// (watch for events callbacks for example...)
useEffect(() => {
if(webglXP) {
webglXP
.onLoading((percent) => {
console.log('loading', percent);
})
.onComplete(() => {
// do what you want (probably dispatch a context event)
});
}
}, [webglXP]);
return (
<div className="WebGLCanvas" ref={container} />
);
};
4、自定義著色器
顯然我必須為這個(gè)網(wǎng)站從頭開始編寫一些自定義著色器。
以下是最有趣的一些細(xì)分。
4.1 著色器塊
如果你仔細(xì)查看上面的示例代碼,會(huì)發(fā)現(xiàn)我允許每個(gè)對(duì)象在需要時(shí)使用自己的自定義著色器。
事實(shí)上,場(chǎng)景中的每個(gè)網(wǎng)格體都使用 ShaderMaterial,因?yàn)楫?dāng)你單擊建筑物時(shí),灰度濾鏡將應(yīng)用于所有其他場(chǎng)景網(wǎng)格體:
應(yīng)用了灰度濾鏡的位置頁面屏幕截圖
這種效果的實(shí)現(xiàn)要?dú)w功于這段超級(jí)簡(jiǎn)單的 glsl 代碼:
const grayscaleChunk = `
vec4 textureBW = vec4(1.0);
textureBW.rgb = vec3(gl_FragColor.r * 0.3 + gl_FragColor.g * 0.59 + gl_FragColor.b * 0.11);
gl_FragColor = mix(gl_FragColor, textureBW, uGrayscale);
`;
由于所有對(duì)象都必須遵守此行為,因此我將其實(shí)現(xiàn)為“著色器塊”,就像 Three.js 最初在內(nèi)部構(gòu)建自己的著色器的方式一樣。
例如,使用的最基本場(chǎng)景的網(wǎng)格片段著色器如下所示:
varying vec2 vUv;
uniform sampler2D map;
uniform float uGrayscale;
void main() {
gl_FragColor = texture2D(map, vUv);
#include <grayscale_fragment>
}
然后我們只獲取材質(zhì)的 onBeforeCompile 方法的一部分:
material.onBeforeCompile = shader => {
shader.fragmentShader = shader.fragmentShader.replace(
"#include <grayscale_fragment>",
grayscaleChunk
);
};
這樣,如果我必須調(diào)整灰度效果,我只需修改一個(gè)文件,它就會(huì)更新我的所有片段著色器。
4.2 云
正如我上面提到的,我們決定不在場(chǎng)景中放置任何真實(shí)的燈光。 但由于云層正在(緩慢)移動(dòng),因此需要對(duì)其應(yīng)用某種動(dòng)態(tài)閃電。
為此,我需要做的第一件事是將頂點(diǎn)世界位置和法線傳遞給片段著色器:
varying vec3 vNormal;
varying vec3 vWorldPos;
void main() {
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * mvPosition;
vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
vNormal = normal;
}
然后在片段著色器中,我使用它們根據(jù)一些uniforms計(jì)算漫反射閃電:
varying vec3 vNormal;
varying vec3 vWorldPos;
uniform float uGrayscale;
uniform vec3 uCloudColor; // emissive color
uniform float uRoughness; // material roughness
uniform vec3 uLightColor; // light color
uniform float uAmbientStrength; // ambient light strength
uniform vec3 uLightPos; // light world space position
// get diffusion based on material's roughness
// see https://learnopengl.com/PBR/Theory
float getRoughnessDiff(float diff) {
float diff2 = diff * diff;
float r2 = uRoughness * uRoughness;
float r4 = r2 * r2;
float denom = (diff2 * (r4 - 1.0) + 1.0);
denom = 3.141592 * denom * denom;
return r4 / denom;
}
void main() {
// ambient light
vec3 ambient = uAmbientStrength * uLightColor;
// get light diffusion
float diff = max(dot(normalize((uLightPos - vWorldPos)), vNormal), 0.0);
// apply roughness
float roughnessDiff = getRoughnessDiff(diff);
vec3 diffuse = roughnessDiff * uLightColor;
vec3 result = (ambient + diffuse) * uCloudColor;
gl_FragColor = vec4(result, 1.0);
#include <grayscale_fragment>
}
這是一種從頭開始應(yīng)用基本閃電陰影的廉價(jià)方法,而且結(jié)果足夠令人信服。
4.3 水中倒影
我花更多時(shí)間寫的片段著色器無疑是波光粼粼的水。
起初,我愿意采用與 Bruno Simon 在 Madbox 網(wǎng)站上所做的類似的方法,但他使用額外的網(wǎng)格和一組自定義 UV 來實(shí)現(xiàn)。
由于 Nico 已經(jīng)忙于所有建模工作,我決定嘗試另一種方法。 我為自己創(chuàng)建了一個(gè)額外的紋理來計(jì)算波的方向:
左:水紋理,右:水流方向紋理
這里,水流方向被編碼在綠色通道中:50% 的綠色表示水流直行,60% 的綠色表示水稍微向左流動(dòng),40% 表示水稍微向右流動(dòng),等等 在…
為了創(chuàng)建波浪,我使用了帶有閾值的 2D perlin 噪聲。 我使用了其他一些 2D 噪聲來確定水會(huì)發(fā)光的區(qū)域,使它們向相反的方向移動(dòng),瞧!
varying vec2 vUv;
uniform sampler2D map;
uniform sampler2D tFlow;
uniform float uGrayscale;
uniform float uTime;
uniform vec2 uFrequency;
uniform vec2 uNaturalFrequency;
uniform vec2 uLightFrequency;
uniform float uSpeed;
uniform float uLightSpeed;
uniform float uThreshold;
uniform float uWaveOpacity;
// see https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83#classic-perlin-noise
// for cnoise function
vec2 rotateVec2ByAngle(float angle, vec2 vec) {
return vec2(
vec.x * cos(angle) - vec.y * sin(angle),
vec.x * sin(angle) + vec.y * cos(angle)
);
}
void main() {
vec4 flow = texture2D(tFlow, vUv);
float sideStrength = flow.g * 2.0 - 1.0;
vec2 wavesUv = rotateVec2ByAngle(sideStrength * PI, vUv) * uFrequency;
float mainFlow = uTime * uSpeed * (1.0 - sideStrength);
float sideFlow = uTime * sideStrength * uSpeed;
wavesUv.x -= sideFlow;
wavesUv.y += mainFlow;
// make light areas travel towards the user
float waveLightStrength = cnoise(wavesUv);
// make small waves with noise
vec2 naturalNoiseUv = rotateVec2ByAngle(sideStrength * PI, vUv * uNaturalFrequency);
float naturalStrength = cnoise(naturalNoiseUv);
// apply a threshold to get small waves moving towards the user
float waveStrength = step(uThreshold, clamp(waveLightStrength - naturalStrength, 0.0, 1.0));
// a light mowing backward to improve overall effect
float light = cnoise(vUv * uLightFrequency + vec2(uTime * uLightSpeed));
// get our final waves colors
vec4 color = vec4(1.0);
color.rgb = mix(vec3(0.0), vec3(1.0), 1.0 - step(waveStrength, 0.01));
// exagerate effect
float increasedShadows = pow(abs(light), 1.75);
color *= uWaveOpacity * increasedShadows;
// mix with original texture
vec4 text = texture2D(map, vUv);
gl_FragColor = text + color;
#include <grayscale_fragment>
}
如果你想測(cè)試一下,這里有一個(gè) Shadertoy 上的演示。
為了幫助我調(diào)試這個(gè)問題,我使用了 GUI 來實(shí)時(shí)調(diào)整所有值并找到最有效的值(當(dāng)然,我已經(jīng)使用該 GUI 來幫助我調(diào)試很多其他事情) 。
4.4 后期處理
最后有一個(gè)使用 Threejs 內(nèi)置 ShaderPass 類應(yīng)用的后處理通道。 它處理出現(xiàn)的動(dòng)畫,在某個(gè)位置聚焦時(shí)在相機(jī)移動(dòng)上添加一點(diǎn)魚眼,并負(fù)責(zé)小級(jí)別校正(亮度、對(duì)比度、飽和度和曝光)。
在放大/縮小動(dòng)畫期間應(yīng)用輕微的后處理變形效果
PostFXShader.js:
const PostFXShader = {
uniforms: {
'tDiffuse': { value: null },
'deformationStrength': { value: 0 },
'showScene': { value: 0 },
// color manipulations
'brightness': { value: 0 },
'contrast': { value: 0.15 },
'saturation': { value: 0.1 },
'exposure': { value: 0 },
},
vertexShader: /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader: `
varying vec2 vUv;
uniform sampler2D tDiffuse;
uniform float showScene;
uniform float deformationStrength;
uniform float brightness;
uniform float contrast;
uniform float saturation;
uniform float exposure;
vec3 adjustBrightness(vec3 color, float value) {
return color + value;
}
vec3 adjustContrast(vec3 color, float value) {
return 0.5 + (1.0 + value) * (color - 0.5);
}
vec3 adjustExposure(vec3 color, float value) {
return color * (1.0 + value);
}
vec3 adjustSaturation(vec3 color, float value) {
// https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
const vec3 luminosityFactor = vec3(0.2126, 0.7152, 0.0722);
vec3 grayscale = vec3(dot(color, luminosityFactor));
return mix(grayscale, color, 1.0 + value);
}
void main() {
vec2 texCoords = vUv;
vec2 normalizedCoords = texCoords * 2.0 - 1.0;
float distanceToCenter = distance(normalizedCoords, vec2(0.0));
vec2 distortedCoords = normalizedCoords * (1.0 - distanceToCenter * deformationStrength);
vec2 offset = normalizedCoords * sin(distanceToCenter * 3.0 - showScene * 3.0) * (1.0 - showScene) * 0.1;
texCoords = (distortedCoords + 1.0) * 0.5 + offset;
vec4 texture = texture2D(tDiffuse, texCoords);
float showEffect = clamp(showScene - length(offset) * 10.0 / sqrt(2.0), 0.0, 1.0);
vec4 grayscale = vec4(1.0);
grayscale.rgb = vec3(texture.r * 0.3 + texture.g * 0.59 + texture.b * 0.11);
texture.rgb = mix(grayscale.rgb, texture.rgb, showEffect);
texture.a = showEffect * 0.9 + 0.1;
texture.rgb *= texture.a;
texture.rgb = adjustBrightness(texture.rgb, brightness);
texture.rgb = adjustContrast(texture.rgb, contrast);
texture.rgb = adjustExposure(texture.rgb, exposure);
texture.rgb = adjustSaturation(texture.rgb, saturation);
gl_FragColor = texture;
}
`
};
export { PostFXShader };
在某些時(shí)候,我們還嘗試添加散景通道,但它對(duì)性能要求太高,因此我們很快就放棄了它。
5、使用 Spector 進(jìn)行調(diào)試
你始終可以通過安裝spector.js擴(kuò)展并檢查WebGL上下文來深入查看使用的所有著色器。
如果你從未聽說過,spector.js 適用于每個(gè) WebGL 網(wǎng)站。 如果想檢查一些 WebGL 效果是如何實(shí)現(xiàn)的,它總是超級(jí)方便!
使用spector.js 調(diào)試片段著色器
6、性能優(yōu)化
我使用了一些技巧來優(yōu)化體驗(yàn)性能。 以下是最重要的兩個(gè):
首先,這應(yīng)該成為一種習(xí)慣:僅在需要時(shí)渲染場(chǎng)景。
這可能聽起來很愚蠢,但它仍然經(jīng)常被低估。 如果你的場(chǎng)景被覆蓋層、頁面或其他任何東西隱藏,就不要繪制它!
renderScene() {
if(this.state.shouldRender) this.animate();
}
我使用的另一個(gè)技巧是根據(jù)用戶 GPU 和屏幕尺寸來調(diào)整場(chǎng)景的像素比。
這個(gè)想法是首先使用 detector-gpu 檢測(cè)用戶的 GPU。 一旦我們獲得了 GPU 估計(jì)的 fps,我們就會(huì)使用實(shí)際屏幕分辨率來計(jì)算實(shí)際條件下該 fps 測(cè)量值的增強(qiáng)估計(jì)。 然后,我們可以根據(jù)每次調(diào)整大小時(shí)的這些數(shù)字來調(diào)整渲染器像素比:
setGPUTier() {
// GPU test
(async () => {
this.gpuTier = await getGPUTier({
glContext: this.renderer.getContext(),
});
this.setImprovedGPUTier();
})();
}
// called on resize as well
setImprovedGPUTier() {
const baseResolution = 1920 * 1080;
this.gpuTier.improvedTier = {
fps: this.gpuTier.fps * baseResolution / (this.width * this.height)
};
this.gpuTier.improvedTier.tier = this.gpuTier.improvedTier.fps >= 60 ? 3 :
this.gpuTier.improvedTier.fps >= 30 ? 2 :
this.gpuTier.improvedTier.fps >= 15 ? 1 : 0;
this.setScenePixelRatio();
}
另一種常見的方法是持續(xù)監(jiān)控給定時(shí)間段內(nèi)的平均 FPS,并根據(jù)結(jié)果調(diào)整像素比。
其他優(yōu)化包括使用或不使用多重采樣渲染目標(biāo),具體取決于 GPU 和 WebGL2 支持(使用 FXAA 通道作為后備)、使用鼠標(biāo)事件發(fā)射器、觸摸和調(diào)整大小事件、使用 gsap 股票代碼作為應(yīng)用程序的唯一 requestAnimationFrame 循環(huán)等 。
7、結(jié)束語
總而言之,我們?cè)跇?gòu)建家鄉(xiāng)的交互式地圖時(shí)度過了一段愉快的時(shí)光。
正如我們所見,打造像這樣的沉浸式 WebGL 體驗(yàn)(需要實(shí)時(shí)渲染很多內(nèi)容)并不困難。 但它確實(shí)需要一些組織和一個(gè)包含多個(gè)文件的干凈代碼庫,可以輕松調(diào)試、添加或刪除功能。
通過該架構(gòu),還可以非常輕松地添加或刪除場(chǎng)景對(duì)象(因?yàn)檫@只是編輯 Javascript 對(duì)象的問題),從而在需要時(shí)可以方便地進(jìn)行進(jìn)一步的站點(diǎn)更新。文章來源:http://www.zghlxwxcb.cn/news/detail-650637.html
原文鏈接:WebGL旅游網(wǎng)站案例研究 — BimAnt文章來源地址http://www.zghlxwxcb.cn/news/detail-650637.html
到了這里,關(guān)于3D沉浸式旅游網(wǎng)站開發(fā)案例復(fù)盤【Three.js】的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!