初学THREE.js,了解基本概念,手写一个示例

308 阅读9分钟

为什么要学习threeJs

随着 Web 技术的发展,视觉,特别是 WebGL/GPU 相关的应用场景会越来越丰富,对技术要求也会越来越高。与前端其他大部分技术不同,WebGL 的上手门槛比较高,需要对数学、图形学有比较扎实的基础,而图形学和视觉呈现技术本身的天花板非常高,未来这块一定会有非常大的发展空间。AI 以及 VR/AR 也是未来前端的发展方向,对于 VR/AR,主流浏览器也开始支持 webXR 技术,应当予以关注,而且无论 AI 还是 XR 这些领域,其实也和 GPU 息息相关,所以它们和可视化技术也是有关联的。

学习资料

  1. 官网文档
  2. 前端代码地址(vue3技术栈实现,放在了文章底部)

threjs基础介绍

  1. 场景THREE.Scene

    渲染器。这里是施展魔法的地方。除了我们在这里用到的WebGLRenderer渲染器之外,Three.js同时提供了其他几种渲染器,当用户所使用的浏览器过于老旧,或者由于其他原因不支持WebGL时,可以使用这几种渲染器进行降级。
    场景是一个容器,是3D效果转换成2D显示的载体。可以在scene中添加3D的开发的控件。

  2. 相机 THREE.PerspectiveCamera 相机的种类很多,不同的场景使用不同的相机。

    /**
     * Cameras 的中种类
     */
    export * from './cameras/StereoCamera';
    export * from './cameras/PerspectiveCamera';
    export * from './cameras/OrthographicCamera';
    export * from './cameras/CubeCamera';
    export * from './cameras/ArrayCamera';
    export * from './cameras/Camera';
    // THREE.PerspectiveCamera(透视摄像机)
    var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
    // 第一个属性是视野角度(FOV)。视野角度就是无论在什么时候,你所能在显示器上看到的场景的范围,它的值是一个角度。
    // 第二个值是长宽比(aspect ratio)。 也就是你用一个物体的宽除以它的高的比值。比如说,当你在一个宽屏电视上播放老电影时,可以看到图像仿佛是被压扁的。
    // 接下来的两个值是远剪切面和近剪切面。 也就是说当物体所在的位置比摄像机的远剪切面远或者所在位置比近剪切面近的时候,该物体超出的部分将不会被渲染到场景中。现在你或许并不用担心这个值的影响,但未来为了获得更好的渲染性能,你将可以在你的应用程序里去设置它。
    
  3. 几何体
    几何体你们知道的。

    // BoxGeometry 三个参数:长、宽、高
    var geometry = new THREE.BoxGeometry( 1, 1, 1 );
    
  4. 物体表面材质 材质有很多,就像是装修房子,不同的东西搭配不同的材质,显示效果才好看。

    export * from './ShadowMaterial';
    export * from './SpriteMaterial';
    export * from './RawShaderMaterial';
    export * from './ShaderMaterial';
    export * from './PointsMaterial';
    export * from './MeshPhysicalMaterial';
    export * from './MeshStandardMaterial';
    export * from './MeshPhongMaterial';
    export * from './MeshToonMaterial';
    export * from './MeshNormalMaterial';
    export * from './MeshLambertMaterial';
    export * from './MeshDepthMaterial';
    export * from './MeshDistanceMaterial';
    export * from './MeshBasicMaterial';
    export * from './MeshMatcapMaterial';
    export * from './LineDashedMaterial';
    export * from './LineBasicMaterial';
    export * from './Material';
    
    // 普通的材质,先上个色
    var material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
    
  5. 网格

    Mesh(网格)。 网格是包含有一个几何体以及应用在在此几何体上的材质的对象,我们可以直接将网格对象放入到我们的场景中,并让它在场景中自由移动。

16418676303586.jpg-w140

**上图就是吧一个3D立方体转换成2D的视图。Mesh就是在一个平面上画网格,并填充材料的容器。**

6. 光源
常见的光源有:太阳、手电筒、办公室的环境光(平行光)等。 ```js /** * Lights */ export * from './lights/SpotLightShadow'; export * from './lights/SpotLight'; export * from './lights/PointLight'; export * from './lights/PointLightShadow'; export * from './lights/RectAreaLight'; export * from './lights/HemisphereLight'; export * from './lights/DirectionalLightShadow'; export * from './lights/DirectionalLight'; export * from './lights/AmbientLight'; export * from './lights/LightShadow'; export * from './lights/Light'; export * from './lights/AmbientLightProbe'; export * from './lights/HemisphereLightProbe'; export * from './lights/LightProbe';

  // 聚光灯 颜色、强度、衰减距离
 var point = new THREE.SpotLight(0xffffff, 1, 500);
  point.position.set(0, -120, 70); // 点光源位置
  point.penumbra = 0.1; // 光影聚焦的百分比 0-1之间,默认值为0,所以阴影会产生锯齿效果.
  point.castShadow = true; // 是否需要增加阴影
  point.shadowCameraVisible = true; // 阴影可见性
  point.shadow.mapSize.width = 1024; // 阴影分辨率
  point.shadow.mapSize.height = 1024; // 阴影分辨率
  point.shadow.camera.near = 0.5; // 阴影产生的最近点
  point.shadow.camera.far = 500; // 阴影产生的最远点
  point.shadow.camera.fov = 20; // 投影视场 投影镜头的视场角是指镜头成像清晰的时候投影画面的对角线的端点与镜头所形成的夹角。标准镜头的视场角一般为20-30°,广角镜头的视场角在60-80°,SC-108镜匹配0.8“LCD投影机时视场角为74°。
  scene.add(point); // 点光源添加到场景中
```

7. 模型

我们日常所使用的东西,大都是不规则的,大多东西都需要建模。threejs支持加载外部自定义模型。

```js
 const loader = new GLTFLoader();
  loader.load(
    "http://websl.data4truth.com:23503/data/web-Project/model/shiba/scene.gltf", // /src/assets/model/shiba/scene.gltf
    function (gltf) {
      gltf.scene.scale.set(6, 6, 6);
      scene.add(gltf.scene);
    },
    undefined,
    function (error) {
      console.error(error);
    }
  );
```

源码

<!--
 * @Description: 页面
 * @Version: 0.0.1
 * @Autor: zhj1214
 * @Date: 2021-12-10 17:56:35
 * @LastEditors: zhj1214
 * @LastEditTime: 2022-01-07 18:14:19
-->
<template>
  <!-- 头部 -->
  <div class="flex-center" style="justify-content: space-between">
    <h1>three.js第一个示例</h1>
    <!-- 换颜色 -->
    <div class="flex-center">
      <n-color-picker
        default-value="#00FF00"
        @update:value="colorComplete"
        :show-alpha="false"
        :modes="['rgb', 'hex', 'hsl', 'hsv']"
        style="width: 140px; height: 42px"
        :swatches="['#FFFFFF', '#18A058', '#2080F0', '#F0A020', 'rgba(208, 48, 80, 1)']"
      />
    </div>
    <!-- 鼠标可以控制视角 -->
    <div class="flex-center">
      <n-space>
        <n-switch :default-value="true" :rail-style="railStyle" @update:value="switchOk">
          <template #checked>开启-鼠标控制</template>
          <template #unchecked>关闭-鼠标控制</template>
        </n-switch>
      </n-space>
    </div>
    <!-- 更换材质 -->
    <div class="flex-center">
      <n-button type="info" dashed @click="changeMaterial">更换材质</n-button>
    </div>
    <!-- 动画开关 -->
    <div class="flex-center">
      <div>动画状态:</div>
      <n-button type="primary" dashed @click="startAnimation">开始</n-button>
      <n-button type="error" style="margin-left: 12px" @click="stopAnimation">暂停</n-button>
    </div>
  </div>
  <!-- 3d视图 -->
  <div ref="threeDemo"></div>
</template>

<script>
import { ref, defineComponent, onMounted } from "vue";
import { NColorPicker, NSwitch, NSpace, NIcon } from "naive-ui";
import { init, createGeometryBox, MaterialBox, renderScene, renderControls } from "./touching";

export default defineComponent({
  components: { NColorPicker, NSwitch, NSpace, NIcon },
  setup() {
    // 获取dom元素
    const threeDemo = ref(null);
    let material; // 材质对象
    // 关闭、打开鼠标控制
    let isSwitch = true;
    const switchOk = isOpen => {
      isSwitch = isOpen;
      console.log(isOpen ? "打开-鼠标控制" : "关闭-鼠标控制");
    };
    // 控制动画的暂停开始
    let isStop = false;
    const stopAnimation = function () {
      isStop = true;
    };
    const startAnimation = function () {
      isStop = false;
      initRenderScene();
    };
    const initRenderScene = () => {
      renderScene(e => {
        if (isStop) {
          cancelAnimationFrame(e);
        }
      });
    };
    // 更换材质颜色
    const colorComplete = color => {
      console.log("selectColor.value", color);
      material.setValues({ color });
    };
    // 更换材质本身
    const changeMaterial = (() => {
      let isChange = false;
      let materialNew;
      return () => {
        if (isChange) {
          const ischangeStyle = !!materialNew;
          materialNew = MaterialBox(ischangeStyle);
          if (ischangeStyle) materialNew = undefined;
        } else {
          const ischangeStyle = !!material;
          material = createGeometryBox(ischangeStyle);
          if (ischangeStyle) material = undefined;
        }
        isChange = !isChange;
      };
    })();
    // 挂载完成
    onMounted(() => {
      // 需要在DOM加载完毕之后才可获取到
      init("demo", threeDemo.value);
      material = createGeometryBox();
      initRenderScene();
      renderControls(controls => {
        if (isSwitch) {
          if (!controls.enableZoom) controls.enableZoom = true;
          if (!controls.enableRotate) controls.enableRotate = true;
          if (!controls.enablePan) controls.enablePan = true;
          controls.update();
        } else {
          // to disable zoom
          controls.enableZoom = false;
          // to disable rotation
          controls.enableRotate = false;
          // to disable pan
          controls.enablePan = false;
        }
      });
    });

    return {
      changeMaterial,
      colorComplete,
      stopAnimation,
      startAnimation,
      threeDemo,
      switchOk,
      railStyle: ({ focused, checked }) => {
        const style = {};
        if (checked) {
          style.background = "#d03050";
          if (focused) {
            style.boxShadow = "0 0 0 2px #d0305040";
          }
        } else {
          style.background = "#2080f0";
          if (focused) {
            style.boxShadow = "0 0 0 2px #2080f040";
          }
        }
        return style;
      },
    };
  },
});
</script>

工具文件js文件

/*
 * @Description:
 * @Version: 0.0.1
 * @Autor: zhj1214
 * @Date: 2021-12-10 17:59:45
 * @LastEditors: zhj1214
 * @LastEditTime: 2022-01-11 10:33:30
 */
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

let scene;
let renderer;
let camera;
let cube;
let controls;
var callBack;
var controlCallBack;

/* ****** 初始化创景和相机 ****** */
export const init = (id, dom) => {
  // 创建场景
  scene = new THREE.Scene();
  /**
   * 创建相机 PerspectiveCamera 代表透视相机 | OrthographicCamera(正交摄像机)。
   * PerspectiveCamera: 这一投影模式被用来模拟人眼所看到的景象,它是3D场景的渲染中使用得最普遍的投影模式
   * OrthographicCamera: 在这种投影模式下,无论物体距离相机距离远或者近,在最终渲染的图片中物体的大小都保持不变。这对于渲染2D场景或者UI元素是非常有用的。
   * */
  // 第一个参数75: 视野角度(FOV)。视野角度就是无论在什么时候,你所能在显示器上看到的场景的范围,它的单位是角度(与弧度区分开)。
  // 第二个参数是长宽比(aspect ratio)。 也就是你用一个物体的宽除以它的高的值。比如说,当你在一个宽屏电视上播放老电影时,可以看到图像仿佛是被压扁的。
  // 接下来的两个参数是近截面(near)和远截面(far)。 当物体某些部分比摄像机的远截面远或者比近截面近的时候,该这些部分将不会被渲染到场景中。或许现在你不用担心这个值的影响,但未来为了获得更好的渲染性能,你将可以在你的应用程序里去设置它。
  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
  camera.position.z = 90; // 移动相机的Z轴坐标
  // 创建渲染器
  renderer = new THREE.WebGLRenderer();
  renderer.shadowMap.enabled = true; // 是否可以投影
  // renderer.setClearColor(0xffffff); // 渲染背景色

  renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap 影子效果
  // 设置渲染器尺寸,第三个参数updateStyle: 分辨率 默认true, 当为false 时,会降低分辨率来显示
  renderer.setSize(window.innerWidth * 0.8, window.innerHeight * 0.8, false);
  dom.appendChild(renderer.domElement);
  // 创建控件对象
  controls = new OrbitControls(camera, renderer.domElement);
};

/* ****** 创建立方体 ****** */
export const createGeometryBox = isChange => {
  if (cube) scene.remove(cube);
  // 创建一个立方体
  let geometry;
  if (isChange) geometry = new THREE.SphereGeometry(30, 50, 50);
  // 球体
  else geometry = new THREE.BoxGeometry(10, 20, 20); // , Math.PI, Math.PI, Math.PI
  // 给立方体添加表面材质,three.js自带了几种材质,在这里我们使用的是MeshBasicMaterial。所有的材质都存有应用于他们的属性的对象。在这里为了简单起见,我们只设置一个color属性,值为0x00ff00,也就是绿色。
  const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 }); // 0x00ff00、'red'、'#867321'
  // 我们需要一个Mesh(网格)。 网格包含一个几何体以及作用在此几何体上的材质,我们可以直接将网格对象放入到我们的场景中,并让它在场景中自由移动。
  cube = new THREE.Mesh(geometry, material);
  // 当我们调用scene.add()的时候,物体将会被添加到(0,0,0)坐标。但将使得摄像机和立方体彼此在一起。为了防止这种情况的发生,我们只需要将摄像机稍微向外移动一些即可。
  // cube.position.set(0, 0, 0);
  cube.castShadow = true; // default is false
  scene.add(cube);

  // 聚光灯
  var point = new THREE.SpotLight(0xffffff, 1, 500);
  point.position.set(0, -120, 70); // 点光源位置
  point.penumbra = 0.1; // 光影聚焦的百分比 0-1之间,默认值为0,所以阴影会产生锯齿效果.
  point.castShadow = true; // 是否需要增加阴影
  point.shadowCameraVisible = true; // 阴影可见性
  point.shadow.mapSize.width = 1024; // 阴影分辨率
  point.shadow.mapSize.height = 1024; // 阴影分辨率
  point.shadow.camera.near = 0.5; // 阴影产生的最近点
  point.shadow.camera.far = 500; // 阴影产生的最远点
  point.shadow.camera.fov = 20; // 投影视场 投影镜头的视场角是指镜头成像清晰的时候投影画面的对角线的端点与镜头所形成的夹角。标准镜头的视场角一般为20-30°,广角镜头的视场角在60-80°,SC-108镜匹配0.8“LCD投影机时视场角为74°。
  scene.add(point); // 点光源添加到场景中

  // 创建一个接收投影的面板
  const planeGeometry = new THREE.PlaneGeometry(400, 500, 320, 320);
  const planeMaterial = new THREE.MeshLambertMaterial({ color: 0x1b17ec });
  const plane = new THREE.Mesh(planeGeometry, planeMaterial);
  plane.receiveShadow = true;
  plane.position.set(0, 0, -50);

  scene.add(plane);

  // 聚光光源助手
  const spotLightHelper = new THREE.SpotLightHelper(point);
  scene.add(spotLightHelper);

  // 创建相机助手
  const helper = new THREE.CameraHelper(point.shadow.camera);
  scene.add(helper);

  return material;
};

/* ****** 更换物体材质 ****** */
export const MaterialBox = isShow => {
  scene.remove(cube);
  // 创建一个立方体
  const geometry = new THREE.BoxGeometry(10, 20, 20);
  const material = new THREE.MeshNormalMaterial({ flatShading: true, wireframe: isShow });

  // 我们需要一个Mesh(网格)。 网格包含一个几何体以及作用在此几何体上的材质,我们可以直接将网格对象放入到我们的场景中,并让它在场景中自由移动。
  cube = new THREE.Mesh(geometry, material);
  cube.castShadow = true; // default is false
  cube.receiveShadow = false; // default
  cube.position.set(0, 0, 0);
  scene.add(cube);
  return material;
};

/* ****** 将物体渲染到场景中 ****** */
// 现在,如果将之前写好的代码复制到HTML文件中,你不会在页面中看到任何东西。这是因为我们还没有对它进行真正的渲染。
// 为此,我们需要使用一个被叫做“渲染循环”(render loop)或者“动画循环”(animate loop)的东西。
export const renderScene = call => {
  // 使立方体动起来
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;
  renderer.render(scene, camera);
  const animationID = requestAnimationFrame(renderScene);
  // 暂停、开启
  if (typeof call === "function") {
    callBack = call;
  } else if (callBack) {
    callBack(animationID);
  }
};

/* ****** 开启投影相机鼠标控制视角 ****** */
export const renderControls = call => {
  // 渲染
  renderer.render(scene, camera);
  requestAnimationFrame(renderControls);
  // 开启视角控制
  controls.update();

  if (typeof call === "function") {
    controlCallBack = call;
  } else if (controlCallBack) {
    controlCallBack(controls);
  }
};