首页 / Three.js 入门教程 / 材质基础

Three.js 入门教程

材质基础

本教程共 40 篇 · 第 12 篇 · 更新于 2026-08-14 · 约 8 分钟阅读

Three.js材质Material透明3D渲染

本节目标:搞懂材质在 three.js 里的角色,掌握所有材质通用的核心属性(颜色、透明、双面、线框等),能独立调出想要的外观。

材质是什么

第 4 章创建 Mesh 时传了两个东西:几何体(Geometry)和材质(Material)。几何体决定”形状长什么样”,材质决定”表面是什么样”:颜色、反光、粗糙度、透明度、是否线框……全都由材质控制。

材质和灯光是配合工作的。有的材质(比如 MeshBasicMaterial)完全不理会灯光,有的材质(比如 MeshStandardMaterial)没有灯就是一片黑。选材质之前,先想清楚”我的物体需不需要被光照亮”。 还要分清材质和纹理的分工:材质决定”属性”,纹理提供”逐点数据”。材质的 color 是整个表面一种颜色;想让表面有木纹、锈迹,就把纹理贴进材质的 map 槽位,由纹理逐像素提供颜色。纹理是材质的输入,第 15 章展开。

所有材质的老祖宗:Material 基类

three.js 里所有材质都继承自 Material 基类,基类定义了一组通用属性,任何材质都有。Material 是基类,直接 new 出来没有任何渲染效果,必须用子类。但”通用属性”这套公共语法,值得先吃透。

color 颜色

几乎所有材质都有 color 属性,传 CSS 颜色字符串、十六进制数、RGB 数组都行:

const material = new THREE.MeshStandardMaterial({ color: 0x4488ff });
material.color.set('#ff6644'); // 或 .set('red')、.set(1, 0.5, 0.2)
Note

MeshNormalMaterial 是个例外:它的颜色由法线方向决定,没有 color 属性(第 13 章细讲)。

side 渲染哪一面

三角形有正面和背面之分,side 控制渲染哪些面:

material.side = THREE.FrontSide;   // 默认:只画正面
material.side = THREE.BackSide;    // 只画背面
material.side = THREE.DoubleSide;  // 两面都画

平面、单层纸张这类形状,从背面看会消失,设成 DoubleSide 即可。代价是渲染的面翻倍,性能敏感时慎用。

transparent 与 opacity:透明

透明度是两个属性配合生效的:

const material = new THREE.MeshStandardMaterial({
  color: 0x44aaff,
  transparent: true, // 关键开关,默认 false
  opacity: 0.5,      // 0 完全透明,1 完全不透明
});
Note

最常见的坑:只设了 opacity 忘了 transparent: true,结果完全不透明。opacity 只在 transparent 为 true 时生效。

透明物体还有一个渲染顺序问题:three.js 按它们到相机的距离排序绘制,两个透明物体互相穿插时,可能出现”谁盖谁”的奇怪效果。这是透明渲染的通病,尽量别让透明物体互相重叠。

wireframe 线框

wireframe: true 时只画三角形的边,不填充面。调试几何体结构、做科技感效果都常用:

const material = new THREE.MeshStandardMaterial({
  color: 0x88ff88,
  wireframe: true,
});
Tip

线框的粗细由 wireframeLinewidth 控制,但大多数浏览器和显卡会忽略它,线宽恒为 1 像素。想要粗线框,只能换思路(比如用 LineSegments 配合 EdgesGeometry)。

visible 可见性

visible: false 时物体不渲染。它和 opacity: 0 有本质区别:前者直接跳过绘制,不占 GPU 时间;后者仍然在渲染,只是看不见。

depthTest 与 depthWrite:深度

depthTest 控制是否做深度测试,默认 true,近处物体遮挡远处物体。关掉后物体会无视遮挡直接画在最前面,做”永远可见”的标记、轮廓时有用。depthWrite 控制是否写入深度缓冲,做 2D 叠加层时常用。这两个属性一般不动,知道存在即可——乱关会导致物体前后关系错乱。

flatShading 平面着色

默认 false:三角形之间的明暗是平滑过渡的(法线做了插值)。设为 true 后每个三角形独立着色,呈现”切面”效果,配多面体、低多边形风格很出效果:

material.flatShading = true;
material.needsUpdate = true; // 通知 three.js 重新编译着色器

其他通用属性一览

Material 基类还有不少属性,先认识名字,用到再查:

  1. emissive / emissiveIntensity:自发光颜色和强度(Lambert、Phong、Standard、Physical 才有,Basic 和 Normal 没有);
  2. blending:混合模式,默认 NormalBlending。粒子、发光特效经常换成 AdditiveBlending(相加混合),颜色会叠加变亮,做火光、光晕很出效果;
  3. alphaTest:透明度低于该值(0~1)的片元直接丢弃,不做半透明混合,做镂空叶片、铁丝网很实用,性能比 transparent 好;
  4. fog:是否受场景雾效影响,默认 true;
  5. needsUpdate:某些属性(如 flatShading)改动后要置 true,触发重新编译;
  6. name:给材质起名字,调试时在场景图里好认。

怎么选材质

three.js 的材质按”是否物理真实”分两派:

  1. 基础材质:MeshBasicMaterial、MeshNormalMaterial、MeshLambertMaterial、MeshPhongMaterial,简单、快、效果有限,下一章细讲;
  2. PBR 材质:MeshStandardMaterial、MeshPhysicalMaterial,物理正确、效果真实、需要灯光配合,第 14 章细讲。

选型可以记一句口诀:要真实感用 Standard;要车漆、玻璃、天鹅绒这类特殊质感用 Physical;只是调试或做扁平风格用 Basic;要性能又要有明暗变化用 Lambert。 还有一点和性能相关:场景里每种材质都会触发一次着色器编译,材质种类越多,首帧卡顿越明显。大量物体同屏时,尽量复用同一个材质实例,而不是每个 Mesh new 一个。

材质对象可以共享

一个材质实例可以同时赋给多个 Mesh:

const material = new THREE.MeshStandardMaterial({ color: 0x44aaff });
const a = new THREE.Mesh(geoA, material);
const b = new THREE.Mesh(geoB, material);

之后修改 material.opacity,a 和 b 会同步变化。需要各自独立外观时,才 new 一个新的材质实例。这和几何体的共享是同一个道理:省内存、改一处全生效,是 three.js 里非常常见的组织方式。

完整示例:透明、双面、线框与平面着色

一个能直接运行的例子,同时演示 color、transparent、opacity、side、wireframe、flatShading: 运行后按住鼠标左键拖拽,从各个角度观察:半透明盒子能看到内部的线框球(透明+深度测试的效果),双面平面转过去仍然可见(side 的作用),线框球只有三角形骨架,多面体每个面边界分明(flatShading 的作用)。一次把本章的六个属性都验证一遍。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>材质基础</title>
  <style>
    html, body { margin: 0; height: 100%; }
  </style>
</head>
<body>
  <script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.module.js",
      "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.185.0/examples/jsm/"
    }
  }
  </script>

  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 100);
    camera.position.set(4, 3, 6);
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(innerWidth, innerHeight);
    document.body.appendChild(renderer.domElement);

    scene.add(new THREE.AmbientLight(0xffffff, 3));
    const dirLight = new THREE.DirectionalLight(0xffffff, 3);
    dirLight.position.set(5, 8, 6);
    scene.add(dirLight);

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

    // 半透明盒子:transparent + opacity
    const glass = new THREE.Mesh(
      new THREE.BoxGeometry(1.2, 1.2, 1.2),
      new THREE.MeshStandardMaterial({
        color: 0x66ccff,
        transparent: true,
        opacity: 0.5,
      })
    );
    glass.position.x = -1.8;
    scene.add(glass);

    // 双面平面:side: DoubleSide,转个角度方便看到背面
    const plane = new THREE.Mesh(
      new THREE.PlaneGeometry(1.6, 1.6),
      new THREE.MeshStandardMaterial({
        color: 0xffaa44,
        side: THREE.DoubleSide,
      })
    );
    plane.position.x = 1.8;
    plane.rotation.y = 0.6;
    scene.add(plane);

    // 线框球:wireframe
    const wire = new THREE.Mesh(
      new THREE.SphereGeometry(0.8, 24, 16),
      new THREE.MeshStandardMaterial({
        color: 0x88ff88,
        wireframe: true,
      })
    );
    wire.position.z = -2;
    scene.add(wire);

    // 平面着色多面体:flatShading
    const gem = new THREE.Mesh(
      new THREE.IcosahedronGeometry(0.9, 0),
      new THREE.MeshStandardMaterial({
        color: 0xcc66ff,
        flatShading: true,
      })
    );
    gem.position.z = 2;
    scene.add(gem);

    function animate() {
      requestAnimationFrame(animate);
      glass.rotation.y += 0.01;
      wire.rotation.y += 0.01;
      gem.rotation.y += 0.01;
      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    addEventListener('resize', () => {
      camera.aspect = innerWidth / innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(innerWidth, innerHeight);
    });
  </script>
</body>
</html>

transparent 改成 false 试试:半透明盒子立刻变成不透明,这就是那个最常见的坑。

常见问题

  1. 物体是全黑的:受光材质(Standard、Lambert 等)没加灯,或者 color 被设成了黑色。先加一盏 AmbientLight 排查。
  2. 设置了 opacity 但完全不透明transparent: true 没开。这是出现频率最高的问题。
  3. 平面从背面看消失了side 默认是 FrontSide,只画正面。改成 DoubleSide 即可。
  4. 改了 flatShading 没反应:材质已经编译过的话,改完要补一句 material.needsUpdate = true
  5. 透明物体互相穿插时前后关系错乱:透明渲染按距离排序,无解时调整物体位置,或尽量减少透明物体的重叠。

小结

材质是”表面属性”的集合。先把 Material 基类的通用属性吃透——color、side、transparent 与 opacity、wireframe、visible、depthTest、flatShading——再学具体材质就快了,因为后面所有材质都共用这套语法。下一章看四种基础材质各自的本事。