用three.js渲染上海外滩模型

635 阅读3分钟

记录一下用three.js加载并渲染上海外滩的BIM模型的小demo

<!DOCTYPE html>
<html lang="en">
  <head>
    <style>
      body {
        margin: 0;
      }
      p {
        position: fixed;
        top: 0;
      }
</style>
    <script src="lib/three.js"></script>
    <script src="lib/OrbitControls.js"></script>
    <script src="lib/GLTFLoader.js"></script>
    <script src="lib/Water.js"></script>
  </head>


  <body>
    <script src="index.js"></script>


    <p>天空:<input type="checkbox" onchange="skyToggle()" /></p>
  </body>
</html>

用到的three官方库:

  • three.min.js:THREE.js WebGL引擎
  • OrbitControls.js:轨道控制,鼠标控制视角变幻
  • GLTFLoader.js:gltf加载库
  • Water.js:水面效果,纯glsl实现

天空的实现:\

天空的实现有多种方式,最常见的是一个包围全部的天空球,通常是UV球,也叫经纬球,其UV很方便映射到一张天空图片,比如:\

天空球的所有面的法线必须朝向圆心(默认是朝外),或者渲染的时候采用背面渲染。\

第二种方式是天空盒,即将上述的天空球变成一个正方体盒子,好处是减少了许多三角面片,只剩12个面,但通常要准备上下左右前后6张图片来贴合天空盒。比如这样:

与这2种方法相比,性能最好的方案是静态天空球(盒),即理想情况下的宇宙背景,天空球的半径无限大,导致渲染的时候,天空不会因为相机的移动而变化,只随旋转而变化,这样减少了许多计算量。\

静态天空球就是360度全景摄像机的原理,它和墨卡托投影有点类似,但是正轴等距圆柱投影,想象一个经纬球,它的经纬线自然展开,UV坐标如下:

可以看到,图中每个矩形的宽高比是1:2,应该把它们都拉伸成正方形,因为赤道:经线=2:1,这样UV贴图的宽高比就是2:1,比如:

图中红线是赤道(圆周),每条竖线是经线(一半圆周),每条纬线都被拉伸至赤道长度。可以看到两极地区被拉伸严重,靠近赤道地区比例正常。\

然后将它通过圆柱投影到球面上,或者转换成6张图作为正方体,作为静态天空,代码如下:

async function skyToggle() {
  if (scene.bgURL === "light.png") {
    scene.bgURL = "dark.png";
  } else {
    scene.bgURL = "light.png";
  }


  const texture = await new THREE.TextureLoader().loadAsync(scene.bgURL);


  const rt = new THREE.WebGLCubeRenderTarget(texture.image.height);
  rt.fromEquirectangularTexture(renderer, texture);
  scene.background = rt.texture;
}

然后是加载gltf模型:\

const loader = new THREE.GLTFLoader();
const modelLoaded = loader
  .loadAsync("shanghai.glb"function (xhr) {
    console.log((xhr.loaded / xhr.total) * 100 + "% loaded");
  })
  .then(function (gltf) {
    console.log("shanghai model:", gltf);
    const { min, max } = new THREE.Box3().setFromObject(gltf.scene);
    gltf.scene.position.sub(
      new THREE.Vector3((min.x + max.x) / 2, min.y, (min.z + max.z) / 2)
    );


    scene.add(gltf.scene);


    gltf.scene
      .getObjectByName("castShadow")
      .traverse((x) => x.isMesh && (x.castShadow = true));
    gltf.scene
      .getObjectByName("ground")
      .traverse((x) => x.isMesh && (x.receiveShadow = true));
  })
  .catch(function (error) {
    console.error(error);
  });

其中让所有的大厦投影,让地面接收投影,实现真实的效果,又避免了所有物体都投影。

其中增加了一个视频plane,作为震旦大厦的大屏动画,plane的4个顶点的UV就是图片的4个角:

const video = document.createElement("video");
    video.src = "test2.mp4";
    video.loop = true;
    video.muted = true;
    video.playbackRate = 0.6;
    video.play();


    const videoPlane = gltf.scene.getObjectByName("video");


    videoPlane.material = new THREE.MeshBasicMaterial({
      map: new THREE.VideoTexture(video),
    });
    videoPlane.geometry.attributes.uv = new THREE.BufferAttribute(
      new Uint8Array([0, 0, 1, 0, 0, 1, 1, 1]),
      2
    );

视频:\

增加海水:

new THREE.TextureLoader().loadAsync("waternormals.jpg").then((waterNormals) => {
  waterNormals.wrapS = waterNormals.wrapT = THREE.RepeatWrapping;


  const water = new THREE.Water(new THREE.PlaneGeometry(1000, 1000), {
    textureWidth: 512,
    textureHeight: 512,
    waterNormals,
    sunDirection: new THREE.Vector3(),
    sunColor: 0xffffff,
    waterColor: 0x001e0f,
    distortionScale: 3.7,
    fog: !!scene.fog,
  });
  water.material.uniforms.size.value = 10;
  water.rotation.x = -Math.PI / 2;


  scene.add(water);
  tasks.push(() => {
    water.material.uniforms.time.value += 0.005;
  });
});

海水法线贴图:

最后再加1个环境光,开启环境光遮蔽,再加一个平行光就好了:\

视频来源:blog.csdn.net/goodriver1