Threejs 适合初学者的教程 ( 3 ) - 基础

139 阅读10分钟

1. 准备工作

先创建基础设施,为开始做好准备,实现最基本的构成以及鼠标可以放大缩小和拖拽换角度:

import * as THREE from "three";

import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";

const renderer = new THREE.WebGLRenderer();

renderer.setSize(window.innerWidth, innerHeight);

document.body.appendChild(renderer.domElement);

const w = window.innerWidth;

const h = window.innerHeight;

// 场景和相机

const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(75, w / h, 0.1, 500);

// 坐标

const axesHelper = new THREE.AxesHelper(5);

scene.add(axesHelper);

camera.position.set(0, 2, 5);

// 网格

const gridHelper = new THREE.GridHelper(30, 30);

scene.add(gridHelper);

// 鼠标缩放 / 旋转角度

const orbit = new OrbitControls(camera, renderer.domElement);

const animate = () => {
  orbit.update();

  renderer.render(scene, camera);

  requestAnimationFrame(animate);
};
animate();

我们使用 vite + vue3 + TypeScript 搭建我们的项目

安装node,官网:url.nodejs.cn/download/

下载之后直接双击安装,建议下载 v18 及以上

打开终端cmd,输入node -v,看到版本,说明已经安装成功了

新建 vite + vue3 + TypeScript 项目:

npm create vue@latest

按照所需要安装的选择进行安装,之后cd进入项目,安装依赖:

npm install

之后拖拽到vscode中运行项目:

npm run dev

之后安装threejs的依赖包:

npm install three
npm install --save-dev @types/three
npm install dat.gui

重启项目,项目中删掉没用的内容,只留一个默认index页面就行

App.vue:

<template>
  <RouterView />
</template>
<script setup lang="ts">
import { RouterView } from "vue-router";
</script>

我们在index.vue中将最上面的代码进行改动和封装,改成适用于vue的语法

index.vue:

<template>
  <div>
    <div ref="threeBoxRef" class="threeBox"></div>
  </div>
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";

// 创建三个场景的容器引用
const threeBoxRef = ref<HTMLElement | null>(null);

// 在组件挂载后初始化 Three.js
onMounted(() => {
  if (threeBoxRef.value) {
    initThreeFn(threeBoxRef.value);
  }
});

// 初始化 Three.js 场景
const initThreeFn = (container: HTMLElement) => {
  const { renderer, scene, camera } = createRendererSceneCamera(container);
  addHelpers(scene);
  addObjectsToScene(scene);
  addResizeListener(renderer, camera);
  animate(renderer, scene, camera);
};

// 创建渲染器、场景和相机
const createRendererSceneCamera = (container: HTMLElement) => {
  const renderer = new THREE.WebGLRenderer();
  renderer.setSize(window.innerWidth, window.innerHeight);
  container.appendChild(renderer.domElement);

  const scene = new THREE.Scene();

  const w = window.innerWidth;
  const h = window.innerHeight;

  const camera = new THREE.PerspectiveCamera(75, w / h, 0.1, 500);
  camera.position.set(10, 25, 25);

  return { renderer, scene, camera };
};

// 添加辅助元素(坐标轴和网格)
const addHelpers = (scene: THREE.Scene) => {
  const axesHelper = new THREE.AxesHelper(5);
  scene.add(axesHelper);

  const gridHelper = new THREE.GridHelper(30, 30);
  scene.add(gridHelper);
};

// 添加物体到场景(地面、几何体)
const addObjectsToScene = (scene: THREE.Scene) => {
  const plane = createPlane();
  scene.add(plane);

  const box = createBox();
  scene.add(box);
};

// 创建平面
const createPlane = () => {
  const geometry = new THREE.PlaneGeometry(30, 30);
  const material = new THREE.MeshBasicMaterial({ color: 0x333333, side: THREE.DoubleSide });
  const plane = new THREE.Mesh(geometry, material);
  plane.rotation.x = -0.5 * Math.PI; // 旋转平面,使其平行于地面
  return plane;
};

// 创建立方体
const createBox = () => {
  const geometry = new THREE.BoxGeometry(6, 6, 6);
  const material = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true });
  const box = new THREE.Mesh(geometry, material);
  box.position.y = 3.01; // 将立方体抬高一点,使其不与地面重叠
  return box;
};

// 设置鼠标控制
const setupOrbitControls = (camera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer) => {
  return new OrbitControls(camera, renderer.domElement);
};

// 动画循环
const animate = (renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.PerspectiveCamera) => {
  const orbit = setupOrbitControls(camera, renderer);

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

  animateFn();
};

// 动态调整窗口大小
const addResizeListener = (renderer: THREE.WebGLRenderer, camera: THREE.PerspectiveCamera) => {
  window.addEventListener('resize', () => {
    if (renderer) {
      renderer.setSize(window.innerWidth, window.innerHeight);
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
    }
  });
};
</script>

<style scoped>
.threeBox {
  width: 100%;
  height: 100vh;
}
</style>

之后运行项目就会看到浏览器中展示:

image.png

2. new THREE.TextureLoader() - 创建纹理加载器实例1:

import skyBlue from "@/assets/images/skyBlue.jpg";

……

// 创建背景渲染
const createBackground = (scene: THREE.Scene) => {
    // 创建一个纹理加载器实例,用于加载图片纹理
    const textureLoader = new THREE.TextureLoader();

    // 使用加载器加载背景纹理(skyBlue 为纹理的图片路径),并将加载后的纹理应用到场景的背景
    scene.background = textureLoader.load(skyBlue);
}

image.png

使用场景:

这种方式常用于创建天空盒子、背景图片等效果,例如加载一个天空图片或自定义的背景来装饰场景。

2. new THREE.CubeTextureLoader() - 创建纹理加载器实例2:

import skyBlue from "@/assets/images/skyBlue.jpg";

……

// 创建背景渲染
const createBackground = (scene: THREE.Scene) => {
  // 创建一个立方体纹理加载器实例
  const cudeTextureLoader = new THREE.CubeTextureLoader();

  // 使用 CubeTextureLoader 加载六个纹理面,给场景设置立方体背景
  scene.background = cudeTextureLoader.load([
    material,  // 正面纹理
    material,  // 背面纹理
    material,  // 左侧纹理
    material,  // 右侧纹理
    material,  // 上侧纹理
    material,  // 下侧纹理
  ]);
}

image.png

使用场景:

此方法通常用于创建 3D 场景中的天空盒或环境贴图。每一面都可以是不同的图像,例如天、地、四面墙等,用于模拟环境或渲染背景。

3. map - 创建几何体贴图

import skyBlue from "@/assets/images/skyBlue.jpg";

……

// 创建立方体
const createBox = () => {
  // 创建纹理加载器实例,用于加载纹理
  const textureLoader = new THREE.TextureLoader();

  // 创建立方体几何体,大小为 6x6x6
  const geometry = new THREE.BoxGeometry(6, 6, 6);

  // 创建材质,设置颜色为红色,同时加载纹理,并禁用线框模式
  const material = new THREE.MeshBasicMaterial({
    color: 0xff0000,  // 立方体的颜色设置为红色
    map: textureLoader.load(skyBlue),  // 使用纹理贴图(skyBlue 图片)
    wireframe: false  // 设置材质为实心(非线框模式)
  });

  // 使用创建的几何体和材质创建立方体网格
  const box = new THREE.Mesh(geometry, material);

  // 设置立方体的位置,使其稍微抬高一点,避免与地面重叠
  box.position.y = 3.01;  // 将立方体的 Y 坐标设置为 3.01

  // 返回创建的立方体
  return box;
};

image.png

也可以简写,从而实现矩形几何体贴图:

// 创建立方体
const createBox = () => {
  const textureLoader = new THREE.TextureLoader();

  const geometry = new THREE.BoxGeometry(6, 6, 6);
  const material = new THREE.MeshBasicMaterial({
    // color: 0xff0000,
    // map: textureLoader.load(skyBlue),
    // wireframe: false
  });
  const box = new THREE.Mesh(geometry, material);
  box.position.y = 3.01; // 将立方体抬高一点,使其不与地面重叠
  
  // 简写贴图
  box.material.map = textureLoader.load(skyBlue);

  return box;
};

image.png

设置每个方向单独贴图

// 创建立方体
const createBox = () => {
  // 创建纹理加载器实例,用于加载纹理贴图
  const textureLoader = new THREE.TextureLoader();

  // 创建立方体几何体,大小为 6x6x6
  const geometry = new THREE.BoxGeometry(6, 6, 6);

  // 创建一个材质列表,分别为立方体的六个面设置不同的纹理
  const boxMaterialList = [
    // 前面贴上 boxMaterial1 纹理
    new THREE.MeshBasicMaterial({ map: textureLoader.load(boxMaterial1) }),

    // 后面贴上 boxMaterial1 纹理
    new THREE.MeshBasicMaterial({ map: textureLoader.load(boxMaterial1) }),

    // 左面贴上 boxMaterial3 纹理
    new THREE.MeshBasicMaterial({ map: textureLoader.load(boxMaterial3) }),

    // 右面贴上 boxMaterial1 纹理
    new THREE.MeshBasicMaterial({ map: textureLoader.load(boxMaterial1) }),

    // 上面贴上 boxMaterial2 纹理
    new THREE.MeshBasicMaterial({ map: textureLoader.load(boxMaterial2) }),

    // 下面贴上 boxMaterial3 纹理
    new THREE.MeshBasicMaterial({ map: textureLoader.load(boxMaterial3) }),
  ];

  // 使用几何体和材质列表创建立方体网格
  const box = new THREE.Mesh(geometry, boxMaterialList);

  // 设置立方体的位置,使其稍微抬高一点,避免与地面重叠
  box.position.y = 18;  // 将立方体的 Y 坐标设置为 18

  // 返回创建的立方体
  return box;
};

image.png

使用场景:

创建一个 6 面不同纹理的立方体,并将其浮动在地面上方。通过这种方式,可以使每个面拥有不同的材质,从而展现丰富的视觉效果。

4. 使用 webGL 的方式绘制几何体

// 常见球形对象创建函数
const createBall = () => {
  // 顶点着色器代码
  const vShader = `
    void main() {
      // 计算顶点的最终位置,投影矩阵和模型视图矩阵用于转换坐标到屏幕空间
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `;

  // 片段着色器代码
  const fShader = `
    void main() {
      // 设置像素颜色为蓝绿色,RGBA 格式,值范围 [0.0, 1.0]
      gl_FragColor = vec4(0.5, 0.25, 1.0, 1.0);  
    }
  `;

  // 创建球体几何体,半径为 4
  const sphereGeometry = new THREE.SphereGeometry(4);

  // 创建着色器材质,将自定义的顶点和片段着色器代码传入
  const sphereMaterial = new THREE.ShaderMaterial({
    vertexShader: vShader,     // 使用自定义顶点着色器
    fragmentShader: fShader    // 使用自定义片段着色器
  });

  // 创建网格对象,将几何体和材质结合
  const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);

  // 设置球体的位置,放置在 (-5, 10, 10)
  sphere.position.set(-5, 10, 10);

  // 返回球体对象,方便添加到场景中
  return sphere;
};

效果图:

image.png

完整的代码:

<template>
  <div>
    <div ref="threeBoxRef" class="threeBox"></div>
  </div>
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";

import skyBlue from "@/assets/images/skyBlue.jpg";
import boxMaterial1 from "@/assets/images/material1.jpeg";
import boxMaterial2 from "@/assets/images/material2.jpeg";
import boxMaterial3 from "@/assets/images/material3.jpeg";

// 创建三个场景的容器引用
const threeBoxRef = ref<HTMLElement | null>(null);

// 在组件挂载后初始化 Three.js
onMounted(() => {
  if (threeBoxRef.value) {
    initThreeFn(threeBoxRef.value);
  }
});

// 初始化 Three.js 场景
const initThreeFn = (container: HTMLElement) => {
  const { renderer, scene, camera } = createRendererSceneCamera(container);
  addHelpers(scene);
  addObjectsToScene(scene);
  addResizeListener(renderer, camera);
  animate(renderer, scene, camera);
};

// 创建渲染器、场景和相机
const createRendererSceneCamera = (container: HTMLElement) => {
  const renderer = new THREE.WebGLRenderer();
  renderer.setSize(window.innerWidth, window.innerHeight);
  container.appendChild(renderer.domElement);

  const scene = new THREE.Scene();

  const w = window.innerWidth;
  const h = window.innerHeight;

  const camera = new THREE.PerspectiveCamera(75, w / h, 0.1, 500);
  camera.position.set(10, 25, 25);

  return { renderer, scene, camera };
};

// 添加辅助元素(坐标轴和网格)
const addHelpers = (scene: THREE.Scene) => {
  const axesHelper = new THREE.AxesHelper(5);
  scene.add(axesHelper);

  const gridHelper = new THREE.GridHelper(30, 30);
  scene.add(gridHelper);
};

// 添加物体到场景(地面、几何体)
const addObjectsToScene = (scene: THREE.Scene) => {
  const plane = createPlane();
  scene.add(plane);

  const box = createBox();
  scene.add(box);

  const ball = createBall();
  scene.add(ball)

  createBackground(scene);
};

// 创建平面
const createPlane = () => {
  const geometry = new THREE.PlaneGeometry(30, 30);
  const material = new THREE.MeshBasicMaterial({ color: 0x333333, side: THREE.DoubleSide });
  const plane = new THREE.Mesh(geometry, material);
  plane.rotation.x = -0.5 * Math.PI; // 旋转平面,使其平行于地面
  return plane;
};

// 创建立方体
const createBox = () => {
  const textureLoader = new THREE.TextureLoader();

  const geometry = new THREE.BoxGeometry(6, 6, 6);

  const boxMaterialList = [
    new THREE.MeshBasicMaterial({ map: textureLoader.load(boxMaterial1) }),
    new THREE.MeshBasicMaterial({ map: textureLoader.load(boxMaterial1) }),
    new THREE.MeshBasicMaterial({ map: textureLoader.load(boxMaterial3) }),
    new THREE.MeshBasicMaterial({ map: textureLoader.load(boxMaterial1) }),
    new THREE.MeshBasicMaterial({ map: textureLoader.load(boxMaterial2) }),
    new THREE.MeshBasicMaterial({ map: textureLoader.load(boxMaterial3) }),
  ]
  const box = new THREE.Mesh(geometry, boxMaterialList);
  box.position.y = 18; // 将立方体抬高一点,使其不与地面重叠

  return box;
};

// 常见球形
const createBall = () => {
  const vShader = `
    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
    }
  `;
  const fShader = `
    void main() {
    gl_FragColor = vec4(0.5, 0.5, 1.0, 1.0);  
  }
  `;

  const sphereGeometry = new THREE.SphereGeometry(4);
  const sphereMaterial = new THREE.ShaderMaterial({
    vertexColors: vShader,
    fragmentShader: fShader
  })
  const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
  sphere.position.set(-5, 10, 10);
  return sphere;
}

// 创建背景渲染
const createBackground = (scene: THREE.Scene) => {
  const textureLoader = new THREE.TextureLoader();
  scene.background = textureLoader.load(skyBlue);
}

// 设置鼠标控制
const setupOrbitControls = (camera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer) => {
  return new OrbitControls(camera, renderer.domElement);
};

// 动画循环
const animate = (renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.PerspectiveCamera) => {
  const orbit = setupOrbitControls(camera, renderer);

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

  animateFn();
};

// 动态调整窗口大小
const addResizeListener = (renderer: THREE.WebGLRenderer, camera: THREE.PerspectiveCamera) => {
  window.addEventListener('resize', () => {
    if (renderer) {
      renderer.setSize(window.innerWidth, window.innerHeight);
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
    }
  });
};
</script>

<style scoped>
.threeBox {
  width: 100%;
  height: 100vh;
}
</style>

5. 在场景中导入三维模型模型文件 - *.glb

import * as THREE from "three";

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';

const horseUrl = new URL("@/assets/images/Horse.glb", import.meta.url);
// 定义导入模型的函数
// 参数:scene 是当前的 Three.js 场景
const importModel = (scene: THREE.Scene) => {
  // 创建 GLTFLoader 实例,用于加载 GLB 或 GLTF 模型
  const assetLoader = new GLTFLoader();

  // 调用 load 方法加载模型
  assetLoader.load(
    horseUrl.href, // 模型文件的路径,使用 horseUrl 提供的 href 属性
    (gltf) => {
      // 当模型加载成功时的回调函数
      const model = gltf.scene; // 获取模型的场景对象

      // 设置模型的位置,将其移动到指定坐标
      model.position.set(-12, 4, 10);

      // 调整模型的缩放比例
      model.scale.set(0.06, 0.06, 0.06);

      // 将模型添加到场景中,使其在渲染器中可见
      scene.add(model);

      // 打印日志,确认模型加载成功并输出模型信息
      console.log("模型加载成功:", model);
    },
    undefined, // 这里可以提供一个函数用于处理加载进度,但当前未实现
    (error) => {
      // 当加载失败时的回调函数
      // 打印错误日志以便排查问题
      console.error("模型加载失败:", error);
    }
  );
};

image.png

完整代码:

<template>
  <div>
    <div ref="threeBoxRef" class="threeBox"></div>
  </div>
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';

import skyBlue from "@/assets/images/skyBlue.jpg";
import boxMaterial1 from "@/assets/images/material1.jpeg";
import boxMaterial2 from "@/assets/images/material2.jpeg";
import boxMaterial3 from "@/assets/images/material3.jpeg";

const horseUrl = new URL("@/assets/images/Horse.glb", import.meta.url);


// 创建三个场景的容器引用
const threeBoxRef = ref<HTMLElement | null>(null);

// 在组件挂载后初始化 Three.js
onMounted(() => {
  if (threeBoxRef.value) {
    initThreeFn(threeBoxRef.value);
  }
});

// 初始化 Three.js 场景
const initThreeFn = (container: HTMLElement) => {
  const { renderer, scene, camera } = createRendererSceneCamera(container);
  addHelpers(scene);
  addObjectsToScene(scene);
  addResizeListener(renderer, camera);
  animate(renderer, scene, camera);
};

// 创建渲染器、场景和相机
const createRendererSceneCamera = (container: HTMLElement) => {
  const renderer = new THREE.WebGLRenderer();
  renderer.setSize(window.innerWidth, window.innerHeight);
  container.appendChild(renderer.domElement);

  const scene = new THREE.Scene();

  const w = window.innerWidth;
  const h = window.innerHeight;

  const camera = new THREE.PerspectiveCamera(75, w / h, 0.1, 500);
  camera.position.set(10, 25, 25);

  return { renderer, scene, camera };
};

// 添加辅助元素(坐标轴和网格)
const addHelpers = (scene: THREE.Scene) => {
  const axesHelper = new THREE.AxesHelper(5);
  scene.add(axesHelper);

  const gridHelper = new THREE.GridHelper(30, 30);
  scene.add(gridHelper);
};

// 添加物体到场景(地面、几何体)
const addObjectsToScene = (scene: THREE.Scene) => {
  const plane = createPlane();
  scene.add(plane);

  const box = createBox();
  scene.add(box);

  const ball = createBall();
  scene.add(ball)

  importModel(scene);

  createBackground(scene);
};

// 创建平面
const createPlane = () => {
  const geometry = new THREE.PlaneGeometry(30, 30);
  const material = new THREE.MeshBasicMaterial({ color: 0x333333, side: THREE.DoubleSide });
  const plane = new THREE.Mesh(geometry, material);
  plane.rotation.x = -0.5 * Math.PI; // 旋转平面,使其平行于地面
  return plane;
};

// 创建立方体
const createBox = () => {
  const textureLoader = new THREE.TextureLoader();

  const geometry = new THREE.BoxGeometry(6, 6, 6);

  const boxMaterialList = [
    new THREE.MeshBasicMaterial({ map: textureLoader.load(boxMaterial1) }),
    new THREE.MeshBasicMaterial({ map: textureLoader.load(boxMaterial1) }),
    new THREE.MeshBasicMaterial({ map: textureLoader.load(boxMaterial3) }),
    new THREE.MeshBasicMaterial({ map: textureLoader.load(boxMaterial1) }),
    new THREE.MeshBasicMaterial({ map: textureLoader.load(boxMaterial2) }),
    new THREE.MeshBasicMaterial({ map: textureLoader.load(boxMaterial3) }),
  ]
  const box = new THREE.Mesh(geometry, boxMaterialList);
  box.position.y = 18; // 将立方体抬高一点,使其不与地面重叠

  return box;
};

// 创建球形
const createBall = () => {
  const vShader = `
    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
    }
  `;
  const fShader = `
    void main() {
    gl_FragColor = vec4(0.5, 0.5, 1.0, 1.0);  
  }
  `;

  const sphereGeometry = new THREE.SphereGeometry(4);
  const sphereMaterial = new THREE.ShaderMaterial({
    vertexColors: vShader,
    fragmentShader: fShader
  })
  const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
  sphere.position.set(-5, 10, 10);
  return sphere;
}

// 创建导入模型
const importModel = (scene: THREE.Scene) => {
  const assetLoader = new GLTFLoader();

  assetLoader.load(
    horseUrl.href,
    (gltf) => {
      const model = gltf.scene;
      model.position.set(-12, 4, 10);
      model.scale.set(0.06, 0.06, 0.06);
      scene.add(model);
      console.log("模型加载成功:", model);
    },
    undefined,
    (error) => {
      console.error("模型加载失败:", error);
    }
  )
}

// 创建背景渲染
const createBackground = (scene: THREE.Scene) => {
  const textureLoader = new THREE.TextureLoader();
  scene.background = textureLoader.load(skyBlue);
}

// 设置鼠标控制
const setupOrbitControls = (camera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer) => {
  return new OrbitControls(camera, renderer.domElement);
};

// 动画循环
const animate = (renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.PerspectiveCamera) => {
  const orbit = setupOrbitControls(camera, renderer);

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

  animateFn();
};

// 动态调整窗口大小
const addResizeListener = (renderer: THREE.WebGLRenderer, camera: THREE.PerspectiveCamera) => {
  window.addEventListener('resize', () => {
    if (renderer) {
      renderer.setSize(window.innerWidth, window.innerHeight);
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
    }
  });
};
</script>

<style scoped>
.threeBox {
  width: 100%;
  height: 100vh;
}
</style>