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

news2024/11/24 8:30:43

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

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

在这里插入图片描述

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

1、打造沉浸式体验

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

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

在这里插入图片描述

2、3D场景

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

2.1 艺术指导和视觉构成

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

在这里插入图片描述

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

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

在这里插入图片描述

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

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

在这里插入图片描述

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

2.2 相机操作

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

在这里插入图片描述

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

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

2.3 烘焙和压缩纹理

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

当然,这意味着更多的建模工作,如果你的场景需要频繁更改,这可能会很烦人。 但它减轻了 GPU 的大量计算负担(光照阴影、阴影贴图……),在我们的例子中,这绝对是值得的。
在这里插入图片描述
在这里插入图片描述

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 (
   <div className="WebGLCanvas" ref={container} />
 );
};

4、自定义着色器

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

4.1 着色器块

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

事实上,场景中的每个网格体都使用 ShaderMaterial,因为当你单击建筑物时,灰度滤镜将应用于所有其他场景网格体:
在这里插入图片描述

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

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

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

material.onBeforeCompile = shader => {
 shader.fragmentShader = shader.fragmentShader.replace(
   "#include <grayscale_fragment>",
   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 <grayscale_fragment>
}

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

4.3 水中倒影

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

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

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

在这里插入图片描述

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

这里,水流方向被编码在绿色通道中: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 <grayscale_fragment>
}

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

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

在这里插入图片描述

4.4 后期处理

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

在这里插入图片描述

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

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 效果是如何实现的,它总是超级方便!
在这里插入图片描述

使用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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/879580.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[足式机器人]Part5 机械设计 Ch00/01 绪论+机器结构组成与连接 ——【课程笔记】

本文仅供学习使用 本文参考&#xff1a; 《机械设计》 王德伦 马雅丽课件与日常作业可登录网址 http://edu.bell-lab.com/manage/#/login&#xff0c;选择观摩登录&#xff0c;查看2023机械设计2。 机械设计-Ch00Ch01——绪论机器结构组成与连接 Ch00-绪论0.1 何为机械设计——…

设计HTML5列表和超链接

在网页中&#xff0c;大部分信息都是列表结构&#xff0c;如菜单栏、图文列表、分类导航、新闻列表、栏目列表等。HTML5定义了一套列表标签&#xff0c;通过列表结构实现对网页信息的合理排版。另外&#xff0c;网页中还包含大量超链接&#xff0c;通过它实现网页、位置的跳转&…

ChatGPT爆火,会给教育带来什么样的影响或者冲击?

近来&#xff0c;人工智能聊天机器人ChatGPT连上热搜&#xff0c;火爆全网。ChatGPT拥有强大的信息整合能力、自然语言处理能力&#xff0c;可谓是“上知天文&#xff0c;下知地理”&#xff0c;而且还能根据要求进行聊天、撰写文章等。 ChatGPT一经推出&#xff0c;便迅速在社…

C语言——动态内存函数(malloc、calloc、realloc、free)

了解动态内存函数 前言&#xff1a;一、malloc函数二、calloc函数三、realloc函数四、free函数 前言&#xff1a; 在C语言中&#xff0c;动态内存函数是块重要的知识点。以往&#xff0c;我们开辟空间都是固定得&#xff0c;数组编译结束后就不能继续给它开辟空间了&#xff0…

机器学习:基本介绍

机器学习介绍 Hnad-crafted rules Hand-crafted rules&#xff0c;叫做人设定的规则。那假设今天要设计一个机器人&#xff0c;可以帮忙打开或关掉音乐&#xff0c;那做法可能是这样&#xff1a; 设立一条规则&#xff0c;就是写一段程序。如果输入的句子里面看到**“turn of…

maven工具-maven的使用-镜像仓库、本地仓、IDEA使用maven

Maven 一、为什么使用maven 添加第三方jar包jar包之间的依赖关系处理jar包之间的冲突获取第三方jar包将项目拆分成多个工程模块实现项目的分布式部署 二、maven简介 ​ Maven项目对象模型(POM)&#xff0c;可以通过一小段描述信息来管理项目的构建&#xff0c;报告和文档的…

B树和B+树区别

B树和B树的区别 B树 B树被称为平衡树&#xff0c;在B树中&#xff0c;一个节点可以有两个以上的子节点。B树的高度为log M N。在B树中&#xff0c;数据按照特定的顺序排序&#xff0c;最小值在左侧&#xff0c;最大值在右侧。 B树是一种平衡的多分树&#xff0c;通常我们说m阶…

Base64编码-算法特别的理解

Base64 在DES加密和AES加密的过程中&#xff0c;加密的编码会出现负数&#xff0c;在ascii码表中找不到对应的字符&#xff0c;就会出现乱码。为了解决乱码的问题&#xff0c;一般结合base64使用 所谓Base64&#xff0c;即是说在编码过程中使用了64种字符&#xff1a;大写A到Z、…

【多线程】网络原理初识

网络原理初识 1. 网络发展史1.2 独立模式1.3 网络互联1.3 局域网1.4 广域网 2. 网络通信基础2.1 IP地址2.2 端口号2.3 认识协议2.4 五元组2.5 协议分层2.5.1 什么是协议分层2.5.2 协议分层的好处2.5.2 OSI七层模型2.5.3 TCP/IP五层模型 2.6 封装和分用2.6.1 封装2.6.1.1 应用层…

MyBatis-Plugin源码全面分析

三、MyBatis-Plugin 1. 基本开发方式 需求&#xff1a;在MyBatis执行之前打印一行醒目的日志&#xff0c;携带参数 实现Interceptor接口&#xff1a; Intercepts(Signature(type Executor.class,method "query",args {MappedStatement.class,Object.class, RowB…

在 Linux 中使用 cp 命令

cp 命令是 Linux 中一个重要的命令&#xff0c;你可能经常会用到它。 正如名称所示&#xff0c;cp 代表 复制copy&#xff0c;它被用于 在 Linux 命令行中复制文件和目录。 这是一个相对简单的命令&#xff0c;只有几个选项&#xff0c;但你仍有必要深入了解它。 在展示 cp …

UML图绘制 -- 类图

1.类图的画法 类 整体是个矩形&#xff0c;第一层类名&#xff0c;第二层属性&#xff0c;第三层方法。 &#xff1a;public- : private# : protected空格: 默认的default 对应的类写法。 public class Student {public String name;public Integer age;protected I…

ardupilot参数的mavlink实现

专业名词释义&#xff0c;参数缩写 gimbal 云台&#xff0c;万向接头 failsafe 故障保护 Collective&#xff1a; 总距 Swashplate &#xff1a; 倾斜盘 SW&#xff1a; Swashplate 倾斜盘 RSC&#xff1a; Rotor Speed Control RC&#xff1a; Radio Channel 无线通道 DDFP&am…

16 dlsys GAN

和有监督的分类工作不同&#xff0c;生成任务的目标更不明确。难以评价生成结果的好坏。 Oracle discriminator 假设我们有一个先知判别器oracle discriminator可以分辨我们生成的内容是真还是假。 D(x) 表示判别数据为真的概率。 我们想让生产成的结果足够真实&#xff0c;所…

前端基础(二)

前言&#xff1a;前端开发框架——Vue框架学习。 准备工作&#xff1a;添加Vue devtools扩展工具 具体可查看下面的这篇博客 添加vue devtools扩展工具添加后F12不显示Vue图标_MRJJ_9的博客-CSDN博客 Vue官方学习文档 Vue.js - 渐进式 JavaScript 框架 | Vue.js MVVM M…

大数据-玩转数据-Flink网页埋点PV统计

一、说明 衡量网站流量一个最简单的指标&#xff0c;就是网站的页面浏览量&#xff08;Page View&#xff0c;PV&#xff09;。用户每次打开一个页面便记录1次PV&#xff0c;多次打开同一页面则浏览量累计。 一般来说&#xff0c;PV与来访者的数量成正比&#xff0c;但是PV并不…

深入理解Linux内核--I/0体系结构和设备驱动程序

I/0体系结构和设备驱动程序 I/O体系结构 为了确保计算机能够正常工作&#xff0c;必须提供数据通路&#xff0c;让信息在连接到个人计算机的CPU、RAM和I/O设备之间流动。 这些数据通路总称为总线&#xff0c;担当计算机内部主通信通道的作用。所有计算机都拥有一条系统总线&a…

Hugging News #0814: Llama 2 学习资源大汇总

每一周&#xff0c;我们的同事都会向社区的成员们发布一些关于 Hugging Face 相关的更新&#xff0c;包括我们的产品和平台更新、社区活动、学习资源和内容更新、开源库和模型更新等&#xff0c;我们将其称之为「Hugging News」。本期 Hugging News 有哪些有趣的消息&#xff0…

时序预测 | MATLAB实现WOA-CNN-BiLSTM鲸鱼算法优化卷积双向长短期记忆神经网络时间序列预测

时序预测 | MATLAB实现WOA-CNN-BiLSTM鲸鱼算法优化卷积双向长短期记忆神经网络时间序列预测 目录 时序预测 | MATLAB实现WOA-CNN-BiLSTM鲸鱼算法优化卷积双向长短期记忆神经网络时间序列预测预测效果基本介绍程序设计学习总结参考资料 预测效果 基本介绍 时序预测 | MATLAB实现…

java练习3.分块查找

题目: 数组 arr{12,43,11,23,54,123,56,12,34} 利用分块排序, 进行从小到大的排序 public class recursionDemo {public static void main(String[] args) {int[] arr{12,43,11,23,54,123,56,12,34};//1.找到无序组 是从哪个元素开始的int startIndex0;for (int i 0; i < …