three.js 项目搭建(一)——实现一个3D大西瓜

6,768 阅读6分钟

用到的库:

创建项目

此处通过React脚手架快速创建一个项目(本篇文章并非主要介绍React,故此处快速开始,不做过多描述,不了解的小伙伴可以查看React官方文档) npx create-react-app tree-test

开始搭建

创建场景

相机、几何体等都需要添加至场景中

import * as THREE from 'three';

// 创建场景
const scene = new THREE.Scene();

创建相机

  //  PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )
  //  fov — 摄像机视锥体垂直视野角度
  //  aspect — 摄像机视锥体长宽比
  //  near — 摄像机视锥体近端面
  //  far — 摄像机视锥体远端面
  const camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
  );
  // 设置相机位置
  camera.position.set(0, 0, 10);
  // 添加至场景中
  scene.add(camera);

添加坐标轴辅助器

  // 添加坐标轴辅助器
  const axesHelper = new THREE.AxesHelper(5);
  scene.add(axesHelper);

添加物体

使用three.js提供的几何体

  // 添加物体
  // BoxGeometry(width : Float, height : Float, depth : Float, widthSegments : Integer, heightSegments : Integer, depthSegments : Integer)
  // width — X轴上面的宽度,默认值为1。
  // height — Y轴上面的高度,默认值为1。
  // depth — Z轴上面的深度,默认值为1。
  // widthSegments — (可选)宽度的分段数,默认值是1。
  // heightSegments — (可选)高度的分段数,默认值是1。
  // depthSegments — (可选)深度的分段数,默认值是1。
  const geometry = new THREE.BoxGeometry(1, 1, 1);
  // 基础网格材质
  const material = new THREE.MeshBasicMaterial({ color: 0x43ad7f1f });
  // 根据几何体和材质创建物体
  const cube = new THREE.Mesh(geometry, material);
  // 添加至场景中
  scene.add(cube);

在图形学中,任何物体都可以由若干个三角形拼接形成,所以我们也可以通过自己绘制三角形拼接形成图形

  const geometry = new THREE.BufferGeometry();
  // 拼成一个矩形至少需要两个三角形,6个点,每个点三个坐标,共18个坐标
  const vertices = new Float32Array([
    -1.1, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0,
    1.0, -1.1, -1.0, 1.0,
  ]);
  // BufferAttribute
  // .array : TypedArray
  // 在 array 中保存着缓存中的数据。
  // .count : Integer
  // 保存 array 除以 itemSize 之后的大小。
  geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
  const material = new THREE.MeshBasicMaterial({ color: 0x43ad7f1f });
  material.wireframe = true;
  const mesh = new THREE.Mesh(geometry, material);
  scene.add(mesh);

效果如图:

image.png

material.wireframe = false;后,图形的前后是有差别的: 从z轴正方向查看: image.png 从z轴负方向查看: image.png

更进一步,我们生成随机半透明的几何图形

  // 添加物体
  for (let i = 0; i < 50; i++) {
    // 每个三角形需要三个顶点,每个顶点需要三个值
    const geometry = new THREE.BufferGeometry();
    // 注意:此处new Float32Array时一定要传入参数,否则图形不会显示
    const positionArray = new Float32Array(9);
    for (let j = 0; j < 9; j++) {
      positionArray[j] = Math.random() * 10 - 5;
    }
    geometry.setAttribute(
      'position',
      new THREE.BufferAttribute(positionArray, 3)
    );
    let color = new THREE.Color(Math.random(), Math.random(), Math.random());
    // 注意:此处一定要设置transparent=true后再设置opacity
    const material = new THREE.MeshBasicMaterial({
      color,
      transparent: true,
      opacity: 0.5,
    });
    const mesh = new THREE.Mesh(geometry, material);
    // material.wireframe = true;
    scene.add(mesh);
  }

效果:

image.png

初始化渲染器及轨道

  // 初始化渲染器
  const renderer = new THREE.WebGLRenderer();
  // 设置渲染的尺寸大小
  renderer.setSize(window.innerWidth, window.innerHeight);
  // 创建轨道控制器
  // OrbitControls( object : Camera, domElement : HTMLDOMElement )
  // object: (必须)将要被控制的相机。该相机不允许是其他任何对象的子级,除非该对象是场景自身。
  
  
  // domElement: 用于事件监听的HTML元素。
  const controls = new OrbitControls(camera, renderer.domElement);
  // 为控制器设置阻尼,让控制器有真实的效果
    controls.enableDamping = true;

渲染

本demo采用React Hook,📢注意renderer.domElement并非React认识的元素,所以需要采用JS原生方式添加子元素(或许是我不知道,很高兴有大佬评论告知其他添加到页面上的写法)

 useEffect(() => {
    const app = document.getElementById('App');
    app?.appendChild(renderer.domElement);
    // 使用渲染器,通过相机将场景渲染进来
    render();
  });
 const render = () => {
    controls.update();
    renderer.render(scene, camera);
    // 此处采用动画是为了响应轨道滑行变换
    requestAnimationFrame(render);
  };

动画

此处简单做了个动画示例,更复杂的动画可以查看gsap

    // 设置动画
    gsap.to(cube.position, { x: 5, duration: 5 });

GUI工具的使用

import * as dat from 'dat.gui';

// 实例化可视化GUI工具   可以通过按 H 键隐藏GUI面板
const gui = new dat.GUI(); //可传递参数{ closed:true ,width:400 }
// gui.hide()  //隐藏GUI面板,可通过按两次 H键开启显示

// 往GUI面板添加要显示的对象的参数
// 参数一:对象;参数二:要调整的对象属性;参数三:最小值;参数四:最大值;参数五:调整精度
// 添加配置 cube y 轴坐标
gui.add(cube.position, 'y', -3, 3, 0.01);
// 添加配置 cube 是否可见
gui.add(cube, 'visible');

const params = {
  color: '#ffff00',
  fn: () => {
    gsap.to(cube.position, { z: 5, duration: 2, yoyo: true, repeat: -1 });
  },
};
// 添加一个色盘,颜色改变时改变cube的颜色
gui.addColor(params, 'color').onChange(value => {
  cube.material.color.set(value);
});
// 添加一个名称为”立方体运动“的设置项,控制是否执行params.fn函数
gui.add(params, 'fn').name('立方体运动');
// 创建一个文件夹,用于存放这些设置
const folder = gui.addFolder('设置立方体');
// 设置 cube 是否显示线框
folder.add(cube.material, 'wireframe');

上述配置之后效果如下图👇🏻

image.png

完整代码

//app.js

import React, { useEffect } from 'react';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import gsap from 'gsap';
import * as dat from 'dat.gui';
// 实例化可视化GUI工具   可以通过按 H 键隐藏GUI面板
const gui = new dat.GUI(); //可传递参数{ closed:true ,width:400 }
// gui.hide()  //隐藏GUI面板,可通过按两次 H键开启显示

function App() {
  // 创建场景
  const scene = new THREE.Scene();

  //   PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )
  //  fov — 摄像机视锥体垂直视野角度
  //  aspect — 摄像机视锥体长宽比
  //  near — 摄像机视锥体近端面
  //  far — 摄像机视锥体远端面
  const camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
  );
  // 设置相机位置
  camera.position.set(0, 0, 10);
  scene.add(camera);

  // 添加物体
  // BoxGeometry(width : Float, height : Float, depth : Float, widthSegments : Integer, heightSegments : Integer, depthSegments : Integer)
  // width — X轴上面的宽度,默认值为1。
  // height — Y轴上面的高度,默认值为1。
  // depth — Z轴上面的深度,默认值为1。
  // widthSegments — (可选)宽度的分段数,默认值是1。
  // heightSegments — (可选)高度的分段数,默认值是1。
  // depthSegments — (可选)深度的分段数,默认值是1。
  const geometry = new THREE.BoxGeometry(1, 1, 1);
  // 基础网格材质
  const material = new THREE.MeshBasicMaterial({ color: 0x43ad7f1f });
  // 根据几何体和材质创建物体
  const cube = new THREE.Mesh(geometry, material);
  scene.add(cube);

  //#region GUI 面板
  // 往GUI面板添加要显示的对象的参数
  // 参数一:对象;参数二:要调整的对象属性;参数三:最小值;参数四:最大值;参数五:调整精度
  gui.add(cube.position, 'y', -3, 3, 0.01);
  gui.add(cube, 'visible');
  const params = {
    color: '#ffff00',
    fn: () => {
      gsap.to(cube.position, { z: 5, duration: 2, yoyo: true, repeat: -1 });
    },
  };
  gui.addColor(params, 'color').onChange(value => {
    cube.material.color.set(value);
  });
  gui.add(params, 'fn').name('立方体运动');
  const folder = gui.addFolder('设置立方体');
  folder.add(cube.material, 'wireframe');
  //#endregion

  // 初始化渲染器
  const renderer = new THREE.WebGLRenderer();
  // 设置渲染的尺寸大小
  renderer.setSize(window.innerWidth, window.innerHeight);
  // 创建轨道控制器
  // OrbitControls( object : Camera, domElement : HTMLDOMElement )
  // object: (必须)将要被控制的相机。该相机不允许是其他任何对象的子级,除非该对象是场景自身。
  // domElement: 用于事件监听的HTML元素。
  const controls = new OrbitControls(camera, renderer.domElement);
  // 为控制器设置阻尼,让控制器有真实的效果
  controls.enableDamping = true;

  // 添加坐标轴辅助器
  const axesHelper = new THREE.AxesHelper(5);
  scene.add(axesHelper);

  // 将webgl渲染的canvas内容添加到页面上
  useEffect(() => {
    const app = document.getElementById('App');
    app?.appendChild(renderer.domElement);
    // 设置动画
    gsap.to(cube.position, { x: 5, duration: 5 });
    // 使用渲染器,通过相机将场景渲染进来
    render();
  });

  window.addEventListener('resize', () => {
    // 更新摄像头
    camera.aspect = window.innerWidth / window.innerHeight;
    // 更新摄像机的投影矩阵
    camera.updateProjectionMatrix();
    // 更新渲染器
    renderer.setSize(window.innerWidth, window.innerHeight);
    // 设置渲染器的像素比
    renderer.setPixelRatio(window.devicePixelRatio);
  });
  window.addEventListener('dblclick', () => {
    const fullScreenElement = document.fullscreenElement;
    if (fullScreenElement) {
      document.exitFullscreen();
    } else {
      renderer.domElement.requestFullscreen();
    }
  });

  const render = () => {
    controls.update();
    renderer.render(scene, camera);
    requestAnimationFrame(render);
  };

  return <div id="App"></div>;
}

export default App;

效果

image.png

进一步加工

导入纹理

为了让看到文章的小伙伴也获得贴图,我就直接放掘金上传图片的url作为示例了

 // 导入纹理
  new THREE.TextureLoader().load(
    'https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/172a9f6fc326477f9a328e64127bc8ed~tplv-k3u1fbpfcp-watermark.image?',
    function (texture) {
      // in this example we create the material when the texture is loaded
      const material = new THREE.MeshStandardMaterial({
        map: texture,
        // 粗糙程度
        roughness: 0.5,
      });
      // 添加物体
      const cubeGeometry = new THREE.SphereGeometry(2, 32, 16);
      const cube = new THREE.Mesh(cubeGeometry, material);
      scene.add(cube);
    },

    // 目前暂不支持onProgress的回调
    undefined,
    // onError回调
    function (err) {
      console.error('An error happened.', err);
    }
  );

另外,一些可以获取贴图的网站:

灯光

  // 灯光
  // 环境光
  const light = new THREE.AmbientLight(0x404040);
  scene.add(light);
  // 直线光
  const lineLight = new THREE.DirectionalLight(0xffffff);
  lineLight.position.set(10, 10, 10);
  scene.add(lineLight);

效果

image.png

题外话

村上春树说,世界上存在着不能流泪的悲伤,这种悲伤无法向人解释,它永远一成不变,如无风夜晚的雪花,静静地沉积在心里,当你虽不认同却只能无奈的屈服,当你不满与现状却又看不清出路,当你充满忧虑却又无力改变,当焦虑变成了每一个异乡人的家常便饭,我们又该如何去完成这一堂人生的必修课。