初见threejs

threejs底层封装了强大的webGL技术,让开发者们可以开箱即用 (其实也并非开箱即用,还是挺麻烦的🙂)。
恰巧朋友遇到了些难题,借此契机,接触了下threejs。
官网是支持中文的,虽然翻译的很差,但寥胜于无。
这里还有个野生的中文文档,感兴趣的可以看看,毕竟也是某位开发者的一腔热血之作!

treejs紧紧围绕了渲染器(Renderer)、场景(Scene)、照相机(Camera),坐标系,物体,光照这些概念,熟悉3d建模或影视特效的人应该非常的熟悉!

下边我就从一个简单的例子,一点点深入。

我们创建一个场景,你可以理解为秀场。
然后再创建一个模型,你可以理解为模特。
最后让模型在场景中展示!

安装

如果你要是使用node或工程化前端项目,直接npm或yarn安装即可

yarn add three

如果你是古老的原始纯html项目,那就cdn引入即可
<version>替换为 Three.js 的实际版本,例如"0.160.0"。最新版本可以在npm 版本列表中找到。

<script src="https://unpkg.com/three@<version>/build/three.module.js"></script>
<!-- 辅助包,非必须,用到再去找即可 -->
<script src="https://unpkg.com/three@<version>/examples/jsm/某个辅助工具.js"></script>

最简单的例子

创建场景

const scene = new THREE.Scene();

创建模型

在threejs中规定,创建模型分3部,具体如下

// 创建物体几何形状
const geometry = new THREE.BoxGeometry(0.4, 0.6, 0.8); // 参数为长宽高

// (基于上一步的几何形状)创建物体外观材质
const material = new THREE.MeshBasicMaterial();

// (基于上一步的外观材质)创建物体网格模型
const mesh = new THREE.Mesh(geometry, material);

创建摄像机

这是什么东西?!
其实也好理解,你自己本身就充当摄像机去看 秀场里的模特。
threejs一切围绕3d,那想了解3d,空间感是必须要有的。
而摄像机就是空间感的表达工具!

const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000);
camera.position.set(1.2, 0, 6); // 设置相机位置

一切就绪,开始展示

 // 创建场景 并将上边的内容都添加到场景中
const scene = new THREE.Scene();
scene.add(mesh);
scene.add(camera);

// 创建一个渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height); //设置three.js渲染区域的尺寸(像素px)
renderer.render(scene, camera); //执行渲染操作

// 执行渲染命令并挂载到页面(会输出一个canvas画布,也就是一个HTML元素,你可以插入到web页面中)
const webglNode = document.querySelector(".webgl");
webglNode.appendChild(renderer.domElement);

优化

可以看到白花花的一片,很难看出立体的感觉,我们来进行下优化

添加三维坐标系

这样可以加深我们对空间的感知度,就像上高数那样🙂
红、绿、蓝分别对应坐标系的x、y、z轴,对于three.js的3D坐标系默认y轴朝上

const axesHelper = new THREE.AxesHelper(150);
scene.add(axesHelper);

换物体外观材质并打灯

可以看到加上三维坐标系之后,确实能提醒我们“这是一个三维物体”。但是直观来说,我看到的还是一坨白!
好在threejs支持多种物品材质,其中就有可以反光的材质,然后我们再打一个灯光,这效果不就上来了?!

// 修改材质为反光材质
const material = new THREE.MeshPhongMaterial();
// 创建灯光
let point = new THREE.PointLight(0xffffff, 1); //创建一个光源: 光的颜色、光的强度
point.position.set(0.6, 0.4, 1.6); //设置光源的位置
const lightHelp = new THREE.PointLightHelper(point, 0.06); // 光源辅助线,便于确认光源位置,后期可以去除
scene.add(point);
scene.add(lightHelp);

注意灯光右很多种,选择合适的即可

const ambientLight = new THREE.AmbientLight(0xab193d, 0.1) // 创建环境光
const dirLight = new THREE.DirectionalLight(0xffffff); // 创建平行光

调整镜头方向

不知道大家有没有发现,入股摄像头移动,但是镜头不做调整的话,物体很快就消失在场景画面中了。
其实这也很好理解,就像电视剧一样,无论涉摄像头怎么移动,演员始终聚集在画面中。
这就是镜头的功劳!

camera.lookAt(0, 0, 0); // 设置镜头方向为坐标原点 其实就是聚焦物体本身上

然后我们调整一下摄像头位置,向上(y轴)移动点,以便看到顶部。在优化下光源位置,看看是不是完美很多

point.position.set(0.6, 1, 1.6); //设置光源的位置
camera.position.set(1.2, 2, 6); // 设置相机位置

动画

这样呆呆的,就像一个静态图一样。
试想一下,如果早知道咱们做出来是静态的,那我直接用ps画出来多好?!
所以,动肯定是要动的。

// 动画封装(这里是旋转90度后,将其旋转回原始位置)
const reRender = ({
  scene,
  camera,
  mesh,
  renderer,
  geometry,
  rotation = THREE.MathUtils.degToRad(90),
}) => {
  let halfwayRotated = true;
  const handel = () => {
    if (!halfwayRotated && mesh.rotation.y < rotation) {
        mesh.rotation.y += 0.01;
    } else {
      halfwayRotated = true;
      if (mesh.rotation.y > 0) {
        mesh.rotation.y -= 0.01;
      } else {
        halfwayRotated = false;
      }
    }
    renderer.render(scene, camera);
    requestAnimationFrame(handel);
  };
  handel();
};
reRender({ scene, camera, mesh, geometry, renderer });

贴图

threejs当然是支持贴图的。

物体本身贴图

从一开始,我就想画一个机箱。
所以我的物体一开始我就设置成了机箱的形状。
核心点:materials.map=loader.load(tietu),

// 创建纹理到
const loader = new THREE.TextureLoader(); // 纹理贴图加载器
const titus = ["../img/zuo.png", "../img/you.png","../img/shang.png","../img/xia.png","../img/qian.png","../img/hou.png"];
// 创建外观材质
const materials = titus.map(tietu=>(new THREE.MeshStandardMaterial({
  map: loader.load(tietu), // 引入纹理
  metalness: 1, //金属光泽:金属度
  roughness: 0, //金属光泽:粗糙度
})));
let point = new THREE.PointLight(0xffffff, 100000); // 调整光源亮度

为配合贴图,我更换了更加合适的材质,并且调整了灯光的亮度

这里是机箱图片素材

截至目前页面全部代码如下

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div class="webgl"></div>
    <script type="module">
      import * as THREE from "./lib/three.module.js";

      // 场景宽高
      const width = 1600; //宽度
      const height = 900; //高度

      // 创建物体几何形状
      const geometry = new THREE.BoxGeometry(0.4, 0.6, 0.8); // 参数为几何物体的长宽高坐标

      // 创建纹理到
      const loader = new THREE.TextureLoader(); // 纹理贴图加载器
      const titus = [
        "../img/zuo.png",
        "../img/you.png",
        "../img/shang.png",
        "../img/xia.png",
        "../img/qian.png",
        "../img/hou.png",
      ];
      // 创建外观材质
      const materials = titus.map(
        (tietu) =>
          new THREE.MeshStandardMaterial({
            map: loader.load(tietu), // 引入纹理
            metalness: 1, //金属光泽:金属度
            roughness: 0, //金属光泽:粗糙度
          })
      );

      // (基于上一步的外观材质)创建物体网格模型
      const mesh = new THREE.Mesh(geometry, materials);

      // 辅助观察的坐标系
      const axesHelper = new THREE.AxesHelper(150);

      // 创建摄像机
      const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000);
      camera.position.set(1.2, 2, 6); // 设置相机位置
      camera.lookAt(0, 0, 0); // 设置镜头方向

      // 创建灯光
      let point = new THREE.PointLight(0xffffff, 100000); //创建一个光源: 光的颜色、光的强度
      point.position.set(0.6, 1, 1.6); //设置光源的位置
      const lightHelp = new THREE.PointLightHelper(point, 0.06); // 光源辅助线,便于确认光源位置,后期可以去除

      // 创建场景 并向场景中添加以上内容
      const scene = new THREE.Scene();
      scene.add(mesh);
      scene.add(camera);
      scene.add(axesHelper);
      scene.add(point);
      scene.add(lightHelp);

      // 创建一个渲染器
      const renderer = new THREE.WebGLRenderer();
      renderer.setSize(width, height); //设置three.js渲染区域的尺寸(像素px)
      renderer.render(scene, camera); //执行渲染操作

      // 执行渲染命令并挂载到页面(会输出一个canvas画布,也就是一个HTML元素,你可以插入到web页面中)
      const webglNode = document.querySelector(".webgl");
      webglNode.appendChild(renderer.domElement);

      // 动画封装(旋转90度后,将其旋转回原始位置)
      const reRender = ({
        scene,
        camera,
        mesh,
        renderer,
        geometry,
        rotation = THREE.MathUtils.degToRad(360),
      }) => {
        let halfwayRotated = true;
        const handel = () => {
          if (!halfwayRotated && mesh.rotation.y < rotation) {
            mesh.rotation.y += 0.01;
          } else {
            halfwayRotated = true;

            if (mesh.rotation.y > 0) {
              mesh.rotation.y -= 0.01;
            } else {
              halfwayRotated = false;
            }
          }
          renderer.render(scene, camera);
          requestAnimationFrame(handel);
        };
        handel();
      };
      reRender({ scene, camera, mesh, geometry, renderer });
    </script>
  </body>
</html>

纹理贴图是异步
对的,纹理贴图的load方法是异步。
我们上边的初始化的时候贴图其实是空的,并没有加载完,因为后期动画的时候又重新reRender了,所以你看起来并无异常。
不然你可以试试把动画关闭,问题即可复现出来:物体会显示为一坨黑色。

怎么优化呢?其实也简单,异步即可。

// 封装的根据图片地址 返回加载后的贴图对象texture
const getTextures = async (imgUrls) => {
  const texturesPromises = imgUrls.map(
    (img) =>
      new Promise((resolve) =>
        loader.load(img, (texture) => resolve(texture))
      )
  );
  const textures = await Promise.all(texturesPromises);
  return textures;
};

const textures = await getTextures([
  "../img/zuo.png",
  "../img/you.png",
  "../img/shang.png",
  "../img/xia.png",
  "../img/qian.png",
  "../img/hou.png",
]);
// 创建外观材质
const materials = textures.map((texture) => {
  return new THREE.MeshStandardMaterial({
    map: texture, // 引入纹理
    metalness: 1, //金属光泽:金属度
    roughness: 0, //金属光泽:粗糙度
  });
});

场景背景贴图

核心点:scene.background = texture;
可以看到如此,如此一来场景(物体四周)便有了贴图画面,有了vr的感觉。
只不过因为没有打灯,物体看起里黢黑,这不重要!

helper.js

import * as THREE from "three";

// 定义一些常量,后边可以简写,也尽可能避免魔法数字
// export const width = window.innerWidth;
// export const height = window.innerHeight;
export const width = 1600; //宽度
export const height = 900; //高度
export const ar = width / height; // Aspect Ratio宽高比

// 创建Cube纹理
export const createCubeTexture = () => {
  const cubeTextureLoader = new THREE.CubeTextureLoader(); // cube纹理加载器
  return new Promise((resolve) => {
    cubeTextureLoader // 加载纹理
      .setPath("./img/grassland/")
      .load(
        ["px.png", "nx.png", "py.png", "ny.png", "pz.png", "nz.png"],
        (res) => {
          resolve(res);
        }
      );
  });
};

// 创建盒子类型的物体(参数为纹理贴图,可以不传)
export const createBoxGeometryMesh = (params) => {
  const geometry = new THREE.BoxGeometry(30, 30, 30); // 创建物体几何形状
  const materials = new THREE.MeshStandardMaterial({ //  创建外观材质
    metalness: 0.8, // 金属度: 默认是0.5,范围是0到1之间。木材或石材用0,金属使用1。
    roughness: 0.06 // 表面粗糙程度: 默认0.5,范围是0~1之间,0表示平滑的镜面反射,1表示完全漫反射
  });
  if (params) {
    const { map, envMap } = params;
    map && (materials.map = map);
    envMap && (materials.envMap = envMap);
  }
  const mesh = new THREE.Mesh(geometry, materials); // 创建物体网格模型
  return mesh;
};

// 渲染页面
export const renderHtml = (scene, camera) => {
  // 创建渲染器
  const renderer = new THREE.WebGLRenderer();
  renderer.setSize(width, height); // 设置渲染的尺寸大小
  renderer.render(scene, camera);

  // 将webgl渲染的canvas内容添加到body
  document.body.appendChild(renderer.domElement);
  return renderer;
};

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div class="webgl"></div>
    <script type="importmap">
      {
        "imports": {
          "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
          "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/",
          "helper": "./helper.js"
        }
      }
    </script>
    <script type="module" async>
      import * as THREE from "three";
      import { OrbitControls } from "three/addons/controls/OrbitControls.js";
      import { ar,createCubeTexture,renderHtml,createBoxGeometryMesh} from "helper";

      // 创建纹理(后边用)
      const texture = await createCubeTexture();

      // 创建场景
      const scene = new THREE.Scene();
      scene.background = texture; // 给场景添加背景

      // 添加坐标轴辅助器,添加到场景中
      const axesHelper = new THREE.AxesHelper(70);
      scene.add(axesHelper);

      // 创建一个物体,添加到场景中
      const mesh = createBoxGeometryMesh();
      scene.add(mesh);

      // 创建相机
      const camera = new THREE.PerspectiveCamera(75, ar, 0.1, 1000);
      camera.position.set(0, 0, 100);

      // 渲染到页面
      const renderer = renderHtml(scene, camera);

      // 设置相机控件轨道控制器
      const controls = new OrbitControls(camera, renderer.domElement);
      controls.addEventListener("change", () => {
        renderer.render(scene, camera);
      });
    </script>
  </body>
</html>

物体反射环境贴图

比如我的物体是一个不锈钢,或者玻璃,那是不是要物体四周要反映出场景的画面 会更真实一些?
关键点:materials.envMap=texture
还是使用上个例子,我把题图传入进去即可(理论上,物体反射贴图和场景题图保持一致,毕竟反射的就是场景画面)

// 创建一个物体,添加到场景中
const mesh = createBoxGeometryMesh({envMap: texture});
scene.add(mesh);

操作鼠标控制物体

想不想拖拽鼠标等操作,即可控制物体或摄像机呢?
当然是可以的!

相机控件轨道控制器 OrbitControls

import { OrbitControls } from "./lib/OrbitControls.js";

// 设置相机控件轨道控制器OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
// 如果OrbitControls改变了相机参数,重新调用渲染器渲染三维场景
controls.addEventListener('change', function () {
    renderer.render(scene, camera); //执行渲染操作
});//监听鼠标、键盘事件

其它API

几何相关

旋转

geometry.rotateY(0.01);

模型相关

注意:
几何的位置相关属性更新它会更新所有顶点位置的值,而模型则内在的顶点位置保持不变,你只是作为一个整体旋转网格。
这是两者的区别,一般情况下使用模型属性来操控位置即可!

位置

mesh.position.[x/y/z]

比如,我设置y轴向上移动,然后在设置x轴往右移动,最后再设置z轴距离我更近些

旋转
比如,下边代码的作用是,将物体以y轴为中心旋转

mesh.rotation.y += 0.01;

摄像机

机位
camera.position.[x/y/z]来控制摄像机位置。
下边代码演示摄像头先向左移动,再向上移动,最后再推近

const reRender = () => {
const handel = () => {
  const cpx = Number(camera.position.x);
  const cpy = Number(camera.position.y);
  const cpz = Number(camera.position.z);
  if(cpx<1.6){
    camera.position.x = cpx+0.01
  }else{
    if(cpy<2.3){
      camera.position.y = cpy+0.01
    }else{
      if(cpz>5){
        camera.position.z = cpz-0.01
      }
    }
  }
  renderer.render(scene, camera);
  requestAnimationFrame(handel);
};
handel();
};
reRender();

镜头
摄像头一半都和镜头搭配使用

let x = 0;
let y = 0;
let z = 0;
const reRender = () => {
  const handel = () => {
    if(x<1){
      x+=0.01
    }else{
      if(y<1){
        y+=0.01
      }else{
        if(z<1){
          z+=0.01
        }
      }
    }
    camera.lookAt(x,y,z)
    renderer.render(scene, camera);
    requestAnimationFrame(handel);
  };
  handel();
};
reRender();

posted @ 2023-12-28 20:53  丁少华  阅读(34)  评论(0编辑  收藏  举报