3D沉浸式旅游网站开发案例复盘【Three.js】

Plongez dans Lyon网站终于上线了。 我们与 Danka 团队和 Nico Icecream 共同努力,打造了一个令我们特别自豪的流畅的沉浸式网站。

这个网站是专为 ONLYON Tourism 和会议而建,旨在展示里昂最具标志性的活动场所。观看简短的介绍视频后,用户可以进入城市的交互式风景如画的地图,所有场馆都建模为 3D 对象。 每个建筑物都可以点击,进入一个详细说明位置信息的专用页面。

3D沉浸式旅游网站开发案例复盘【Three.js】_第1张图片

推荐:用 NSDT编辑器 快速搭建可编程3D场景。

1、打造沉浸式体验

主要网站导航体验依赖于卡通般的 WebGL 场景,其中包含大量景观元素、云彩、动画车辆、波光粼粼的河流,当然还有建筑物。

总而言之,它由 63 个几何图形、48 个纹理、32234 个三角形(以及一些后期处理魔法)组成。 当你处理大量对象时,必须组织代码架构并使用一些技巧来优化性能。

3D沉浸式旅游网站开发案例复盘【Three.js】_第2张图片

2、3D场景

所有模型均由才华横溢的 3D 艺术家 Nicolas Dufoure(又名 Icecream)在 3ds Max 中创建,然后使用 Blender 导出为 GTLF 对象。如果你有一些现成的3D模型可以利用,那么可以使用这个在线3D格式转换工具将它们转换成GLTF模型,这会节省不少时间。

2.1 艺术指导和视觉构成

Nico 和 Danka 团队从地图的早期迭代开始了项目的创作过程,并很快确定了低多边形和丰富多彩的艺术方向。

3D沉浸式旅游网站开发案例复盘【Three.js】_第3张图片

与客户品牌调色板相匹配的早期地图迭代之一

我们知道必须添加两打可点击的建筑物,因此我们必须在视觉构图、导航便利性和性能之间找到适当的平衡。

3D沉浸式旅游网站开发案例复盘【Three.js】_第4张图片

左:第一个场景合成测试渲染,右:早期 webgl 压力测试

为了将绘制的三角形数量保持在最低限度,我们还很快决定限制场景左侧和右侧远侧的 3D 对象的数量。 但过了一段时间,我们意识到我们实际上必须阻止用户看到这些区域。

3D沉浸式旅游网站开发案例复盘【Three.js】_第5张图片

这个地方看起来很空,不是吗?

2.2 相机操作

为了避免平移、缩放和动画之间的任何冲突,我很早就决定从头开始编写相机控件的代码。 事实证明这非常方便,因为之后为相机可能的位置添加阈值并不困难。

3D沉浸式旅游网站开发案例复盘【Three.js】_第6张图片

白色三角形代表我们实际的相机范围

这样,我们成功地限制了相机的移动,同时仍然允许用户探索所有地图重要区域。

2.3 烘焙和压缩纹理

为了节省大量 GPU 工作负载,Nico 和我同意的另一件事是用全局照明和阴影烘焙所有纹理。

当然,这意味着更多的建模工作,如果你的场景需要频繁更改,这可能会很烦人。 但它减轻了 GPU 的大量计算负担(光照阴影、阴影贴图……),在我们的例子中,这绝对是值得的。
3D沉浸式旅游网站开发案例复盘【Three.js】_第7张图片
3D沉浸式旅游网站开发案例复盘【Three.js】_第8张图片

3D场景建模概述

当处理如此数量的纹理(通常为 1024x1024、2048x2048 甚至 4096x4096 像素宽)时,你应该考虑的另一件事是使用基础压缩纹理。

如果你从未听说过,基础纹理基本上比 jpeg/png 纹理占用更少的 GPU 内存。 当它们从 CPU 上传到 GPU 时,它们还可以降低主线程瓶颈。

你可以在这里非常轻松地生成基础纹理。

3、代码架构和组织

当需要处理如此多的资源时,组织代码的最佳方法是创建几个 javascript 类(或函数,当然取决于你)并将它们组织在目录和文件中。

通常,我是这样组织该项目的文件和文件夹的:

webgl
|-- data
|   |-- objects.js
|   |-- otherObjects.js
|-- shaders
|   |-- customShader.js
|   |-- anotherShader.js
|-- CameraController.js
|-- GroupRaycaster.js
|-- ObjectsLoader.js
|-- WebGLExperience.js
  • data文件夹包含单独文件中的 javascript 对象以及所有信息
  • shaders文件夹包含单独文件中的所有项目自定义着色器
  • CameraController.js:处理所有相机移动和控制的类
  • GroupRaycaster.js:处理所有“交互式”对象光线投射的类
  • ObjectsLoader.js:加载所有场景对象的类
  • WebGLExperience.js:初始化渲染器、相机、场景、后处理并处理所有其他类的主类

当然,你可以自由地以不同的方式组织它。 例如,有些人喜欢为渲染器、场景和相机创建单独的类。

3.1 核心的概念代码摘录

那么让我们进入代码本身吧!

以下是一些文件实际外观的详细示例。

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 集成

由于该项目使用 Nextjs,我们需要在 React 组件内实例化我们的 WebGLExperience 类。

我们只需创建一个 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 (
   
); };

4、自定义着色器

显然我必须为这个网站从头开始编写一些自定义着色器。
以下是最有趣的一些细分。

4.1 着色器块

如果你仔细查看上面的示例代码,会发现我允许每个对象在需要时使用自己的自定义着色器。

事实上,场景中的每个网格体都使用 ShaderMaterial,因为当你单击建筑物时,灰度滤镜将应用于所有其他场景网格体:
3D沉浸式旅游网站开发案例复盘【Three.js】_第9张图片

应用了灰度滤镜的位置页面屏幕截图

这种效果的实现要归功于这段超级简单的 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);
`;

由于所有对象都必须遵守此行为,因此我将其实现为“着色器块”,就像 Three.js 最初在内部构建自己的着色器的方式一样。

例如,使用的最基本场景的网格片段着色器如下所示:

varying vec2 vUv;

uniform sampler2D map;
uniform float uGrayscale;

void main() {
 gl_FragColor = texture2D(map, vUv);

 #include 
}

然后我们只获取材质的 onBeforeCompile 方法的一部分:

material.onBeforeCompile = shader => {
 shader.fragmentShader = shader.fragmentShader.replace(
   "#include ",
   grayscaleChunk
 );
};

这样,如果我必须调整灰度效果,我只需修改一个文件,它就会更新我的所有片段着色器。

4.2 云

正如我上面提到的,我们决定不在场景中放置任何真实的灯光。 但由于云层正在(缓慢)移动,因此需要对其应用某种动态闪电。

为此,我需要做的第一件事是将顶点世界位置和法线传递给片段着色器:

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;
}

然后在片段着色器中,我使用它们根据一些uniforms计算漫反射闪电:

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 
}

这是一种从头开始应用基本闪电阴影的廉价方法,而且结果足够令人信服。

4.3 水中倒影

我花更多时间写的片段着色器无疑是波光粼粼的水。

起初,我愿意采用与 Bruno Simon 在 Madbox 网站上所做的类似的方法,但他使用额外的网格和一组自定义 UV 来实现。

由于 Nico 已经忙于所有建模工作,我决定尝试另一种方法。 我为自己创建了一个额外的纹理来计算波的方向:

3D沉浸式旅游网站开发案例复盘【Three.js】_第10张图片

左:水纹理,右:水流方向纹理

这里,水流方向被编码在绿色通道中:50% 的绿色表示水流直行,60% 的绿色表示水稍微向左流动,40% 表示水稍微向右流动,等等 在…

为了创建波浪,我使用了带有阈值的 2D perlin 噪声。 我使用了其他一些 2D 噪声来确定水会发光的区域,使它们向相反的方向移动,瞧!

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 
}

如果你想测试一下,这里有一个 Shadertoy 上的演示。

为了帮助我调试这个问题,我使用了 GUI 来实时调整所有值并找到最有效的值(当然,我已经使用该 GUI 来帮助我调试很多其他事情) 。

3D沉浸式旅游网站开发案例复盘【Three.js】_第11张图片

4.4 后期处理

最后有一个使用 Threejs 内置 ShaderPass 类应用的后处理通道。 它处理出现的动画,在某个位置聚焦时在相机移动上添加一点鱼眼,并负责小级别校正(亮度、对比度、饱和度和曝光)。

3D沉浸式旅游网站开发案例复盘【Three.js】_第12张图片

在放大/缩小动画期间应用轻微的后处理变形效果

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 };

在某些时候,我们还尝试添加散景通道,但它对性能要求太高,因此我们很快就放弃了它。

5、使用 Spector 进行调试

你始终可以通过安装spector.js扩展并检查WebGL上下文来深入查看使用的所有着色器。

如果你从未听说过,spector.js 适用于每个 WebGL 网站。 如果想检查一些 WebGL 效果是如何实现的,它总是超级方便!
3D沉浸式旅游网站开发案例复盘【Three.js】_第13张图片

使用spector.js 调试片段着色器

6、性能优化

我使用了一些技巧来优化体验性能。 以下是最重要的两个:

首先,这应该成为一种习惯:仅在需要时渲染场景。

这可能听起来很愚蠢,但它仍然经常被低估。 如果你的场景被覆盖层、页面或其他任何东西隐藏,就不要绘制它!

renderScene() {
 if(this.state.shouldRender) this.animate();
}

我使用的另一个技巧是根据用户 GPU 和屏幕尺寸来调整场景的像素比。

这个想法是首先使用 detector-gpu 检测用户的 GPU。 一旦我们获得了 GPU 估计的 fps,我们就会使用实际屏幕分辨率来计算实际条件下该 fps 测量值的增强估计。 然后,我们可以根据每次调整大小时的这些数字来调整渲染器像素比:

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();
}

另一种常见的方法是持续监控给定时间段内的平均 FPS,并根据结果调整像素比。

其他优化包括使用或不使用多重采样渲染目标,具体取决于 GPU 和 WebGL2 支持(使用 FXAA 通道作为后备)、使用鼠标事件发射器、触摸和调整大小事件、使用 gsap 股票代码作为应用程序的唯一 requestAnimationFrame 循环等 。

7、结束语

总而言之,我们在构建家乡的交互式地图时度过了一段愉快的时光。

正如我们所见,打造像这样的沉浸式 WebGL 体验(需要实时渲染很多内容)并不困难。 但它确实需要一些组织和一个包含多个文件的干净代码库,可以轻松调试、添加或删除功能。

通过该架构,还可以非常轻松地添加或删除场景对象(因为这只是编辑 Javascript 对象的问题),从而在需要时可以方便地进行进一步的站点更新。


原文链接:WebGL旅游网站案例研究 — BimAnt

你可能感兴趣的:(3d,旅游,javascript)