【Three.js】day99-three-scene-basics

作者:mario 发布时间: 2026-09-07 阅读量:6 评论数:0

Day 99 · 场景基础 — Scene / Camera / Renderer:第一个 3D 物体

Three.js 的第一课,也是 3D 世界里最朴素的真理:没有相机,世界存在但你看不见;没有光照,世界存在但你看不清。今天搭起最小组件——场景(容器)、相机(眼睛)、渲染器(画布)、一个立方体(物体)——然后彻底理解三件事:坐标系约定、为什么"物体不见了"是最常见的坑、以及你与 2D 世界的第一个对照(canvas 上下文 vs 渲染器)。


目录


一、工程准备与最小场景

# 在大屏工程里加 3D 能力(或建孪生子工程,见 Week 16 决策)
npm install three
npm install -D @types/three   # TS 类型

最小场景(不依赖 Vue,先看纯 Three.js 逻辑):

// src/core/three/mini-scene.ts(纯逻辑:不 import vue——Day 85 的 core/ 纪律)
import * as THREE from "three";

/**
 * 搭建最小 3D 场景:世界(Scene)+ 眼睛(PerspectiveCamera)+ 画布(WebGLRenderer)
 * 三件套缺一不可,职责:
 *   Scene       —— 容器:装物体、灯光、相机
 *   Camera      —— 观察者:决定"从哪看、看多广"
 *   Renderer    —— 绘制者:把场景按相机视角画到 canvas
 */
export function createMiniScene(canvas: HTMLCanvasElement): {
  scene: THREE.Scene;
  camera: THREE.PerspectiveCamera;
  renderer: THREE.WebGLRenderer;
  /** 一次性静态渲染(Day 100 换成持续渲染循环) */
  renderOnce(): void;
} {
  // 1. 世界
  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0x0d1421);   // 大屏底色(Day 66 token 的 3D 版)

  // 2. 眼睛:透视相机
  //    参数:视野 75° / 宽高比(自适应) / 近裁面 0.1 / 远裁面 1000
  const camera = new THREE.PerspectiveCamera(
    75,
    canvas.clientWidth / canvas.clientHeight,
    0.1,
    1000
  );
  camera.position.set(3, 2, 5);   // 站在 (3,2,5),看向原点(默认 -Z 方向)
  camera.lookAt(0, 0, 0);

  // 3. 画布:WebGL 渲染器
  const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
  renderer.setSize(canvas.clientWidth, canvas.clientHeight);

  // 4. 物体:立方体(几何 + 材质 = 网格 Mesh)
  const geometry = new THREE.BoxGeometry(1, 1, 1);
  const material = new THREE.MeshStandardMaterial({ color: 0x00c6ff });   // 主色 token
  const cube = new THREE.Mesh(geometry, material);
  scene.add(cube);

  // 5. 光照:没有灯,Standard 材质是黑的(第三节/第四节)
  //    ——先加一盏环境光,让物体"能看见":
  scene.add(new THREE.AmbientLight(0xffffff, 0.6));

  return {
    scene,
    camera,
    renderer,
    renderOnce() {
      renderer.render(scene, camera);   // 核心一行:把世界画出来
    },
  };
}

🎯 核心一行renderer.render(scene, camera)。3D 的一切最终都收敛到这一行——场景里有什么、相机怎么看、就被画成什么。今天它只调一次(静态图),Day 100 把它放进 rAF 循环。


二、三件套职责:世界、眼睛、画布

组件 2D 对应物 职责 关键属性
Scene 一个"世界容器" 装所有物体/灯光/相机 backgroundadd()
PerspectiveCamera 你的眼睛 决定视角:视野、远近裁剪、位置朝向 fov/aspect/near/farpositionlookAt
WebGLRenderer canvas 2D 上下文 把世界投影成画面 setSizerender(scene, camera)

相机的三个经典问题(本周必答)

Q1 为什么有"近裁面/远裁面"?
   GPU 只绘制 near~far 之间的物体——省算力,也是"物体突然消失"的坑源之一
Q2 lookAt 是干嘛的?
   相机默认朝 -Z 看;lookAt 让它对准某个点——没调用时可能"看向虚空"
Q3 相机在物体内部?
   物体会被裁掉一部分(近裁面原理)——"只看得到一片黑/一片面"的坑源

三、坐标系:3D 的第一门语言

Three.js 用右手坐标系

      +Y(上)
      |
      |___ +X(右)
     /
  +Z(朝你/屏幕外)

约定:
  - 默认相机朝 -Z 方向看(看向屏幕里)
  - 地平面通常放 y=0,物体往上长
  - 旋转:正方向 = 从轴正端看向原点的逆时针

与 2D 的对照:Canvas 2D 的 y 轴向下,Three.js 的 y 轴向上——第一天就会踩的"上下颠倒"坑。记忆钩子:3D 世界"站起来"了,y 从"往下"变成"往上"。


四、为什么"物体不见了"(三步排查法)

3D 新手 90% 的问题都是"黑屏/白屏/物体消失"。固定排查顺序(今天建立,未来全靠它):

第 1 步:相机看得见吗?
   - position 是否在物体附近?(别在物体内部)
   - lookAt 是否对准了物体?
   - near/far 是否包含物体距离?(物体离相机 > far 或 < near 都会被裁)
第 2 步:光照与材质匹配吗?
   - MeshBasicMaterial 不需要光(自发光感)
   - MeshStandardMaterial/Phong 需要光——没灯 = 全黑
   - 检查是否有 AmbientLight 兜底
第 3 步:渲染器画了吗?
   - render(scene, camera) 被调了吗?(Day 100 的循环问题)
   - canvas 尺寸是否为 0?(容器未布局/display:none——Day 76 同款坑)

把这三步背下来——它是你未来 1000 次的排错起点,也是面试"3D 黑屏怎么排查"的标准答案骨架。


五、放进 Vue:第一个 3D 组件

<!-- src/components/ThreeViewport.vue(最小 3D 容器,Day 103 全面边界化) -->
<script setup lang="ts">
/**
 * 3D 视口组件:只负责"把 Three.js 场景挂进 Vue 世界"
 * 今天的简化版——renderer/scene 实例普通变量持有(Day 87 markRaw 纪律的预演)
 */
import { onMounted, onUnmounted, ref } from "vue";
import { createMiniScene } from "../core/three/mini-scene";

const canvas = ref<HTMLCanvasElement>();   // 模板引用:canvas 挂载点
let dispose: (() => void) | null = null;   // 清理函数(成对纪律)

onMounted(() => {
  const mini = createMiniScene(canvas.value!);
  mini.renderOnce();                        // 静态渲染(Day 100 升级为循环)

  // 清理协议:成对纪律(Day 69/82 的 3D 版)
  // ——渲染器 dispose 释放 GPU 上下文,否则内存/显存泄漏
  dispose = () => {
    mini.renderer.dispose();
  };
});

onUnmounted(() => dispose?.());
</script>

<template>
  <canvas ref="canvas" class="three-viewport" />
</template>

<style scoped>
.three-viewport { width: 100%; height: 100%; display: block; }
</style>

为什么要手动 dispose:WebGLRenderer 占用 GPU 资源,组件卸载不释放会泄漏显存——这是 3D 世界的"Day 69 泄漏排查"提前预习(Day 105 会系统测)。


六、常见坑点

坑 1:canvas 尺寸为 0(渲染器白屏)

容器在 display:none 或未布局时创建渲染器 → setSize(0,0)。修法:setSize 前确认 clientWidth > 0,或用 ResizeObserver(Day 76/86 的同款协议,Day 105 统一)。

坑 2:材质用 MeshStandardMaterial 但没加光

全黑是"没灯"最典型的症状(不是 bug)。新手最容易漏 AmbientLight。口诀:Standard 材质 = 需要光

坑 3:相机在物体内部

camera.position.set(0,0,0) + 立方体在原点 → 相机在立方体内部 → 只看到内表面/一片面。排查第 1 步的第一问。

坑 4:忘调 render()

最基础的"白屏":场景建好了、相机摆好了,就是没画。今天只调一次看不出循环问题——Day 100 会放大它。

坑 5:忘清理(提前踩)

组件卸载不 dispose → 多次进出页面显存涨。今天的 dispose 回调用上,Day 105 用 Performance 验证。


七、自测挑战

T1 · 最小场景(60 分钟)

完成第一至五节:一个青色立方体出现在深色背景中央,颜色与 Day 66 token 一致。调整相机位置(左右/远近)观察视角变化。

T2 · 三步排查演练(30 分钟)

故意制造三个故障并记录现象:① 删掉 AmbientLight(全黑)② 相机移到 (0,0,0)(看内面)③ 注释 renderOnce()(白屏)。亲手错三次,排查法才长在身上

T3 · 坐标系实验(20 分钟)

把立方体放到 (2,0,0)(右)、(0,2,0)(上)、(0,0,2)(朝屏幕外)三个位置,截图记录每个位置在画面中的去向——建立"代码坐标 → 画面位置"的直觉。


八、总结

环节 要点
三件套 Scene 世界 / Camera 眼睛 / Renderer 画布——缺一不可
核心一行 renderer.render(scene, camera)
坐标系 右手系,y 向上——与 2D 的 y 向下对照
三步排查 相机看得见吗 → 光照材质匹配吗 → 渲染器画了吗
Vue 挂载 普通变量持有 + dispose 成对——Day 87 纪律预演

世界搭好了,立方体站在那里。明天让它动起来:渲染循环与动画纪律。

评论