Three.js-硬要自学系列33之专项学习基础材质

90 阅读8分钟

什么是基础材质

想象我们给一个3D模型(比如一个立方体或球体)涂颜色。基础材质就像是用一种固定颜色的油漆直接刷上去,它完全忽略光线的影响。也就是说,无论场景中有没有灯光,物体看起来都一样亮,颜色不会变深或变浅。

如果我们给材质设置为红色,那么物体在任何角度下都是均匀的红色,不会有阴影或高光(高光是指物体表面反光时的亮点)。其效果会显得‘扁平’,不像真实物体那样有立体感。

为什么称其为基础材质

因为它简单直接,但有限制

  • 优点:超级简单,不需要设置灯光。我们在做一些卡通风格、UI元素或背景物体,基础材质是完美的选择。比如,创建一个不需要真实感的标志或简单的3D文字时,它速度快、性能好(对电脑负担小)。
  • 缺点:因为它忽略光线,物体看起来不够真实。如果我们想让物体有“金属感”或“塑料感”,基础材质就做不到。例如,一个球体在灯光下应该有亮部和暗部,但基础材质会让它像一张剪纸一样平坦。
  • 对比其他材质:three.js 还有更高级的材质,比如MeshLambertMaterial(它会响应光线,让物体有明暗变化)或MeshPhongMaterial(能模拟高光,像反光的塑料)。

适用场景

  • 简单原型或测试:当我们想快速搭建一个3D场景时,基础材质能节省省时间。比如,先做个草稿,再换成更高级的材质。
  • 非真实渲染(NPR) :像卡通动画、游戏中的低多边形风格,基础材质能营造出“手绘”效果。
  • 性能优化:如果网页需要加载很多物体,基础材质消耗资源少,能提升流畅度。但记住,它不支持纹理贴图(比如给物体贴图片),只能纯色或简单渐变。

顶点着色案例

效果如图

1.gif

需要了解的API

MeshBasicMaterial

最基础的材质类型,无视光照,恒定显示颜色/纹理。  适用于简单模型、UI元素或性能敏感场景。

以下是一些常用的属性

属性类型默认值作用示例值
colorTHREE.Color0xffffff (白色)材质基础色new THREE.Color('skyblue')
mapTHREE.Texturenull表面贴图纹理new THREE.TextureLoader().load('brick.jpg')
wireframebooleanfalse是否显示为线框true (网格线效果)
opacitynumber1.0透明度 (需开启transparent)0.5 (半透明)
transparentbooleanfalse是否允许透明设为trueopacity生效
sideenumTHREE.FrontSide渲染面THREE.DoubleSide (双面渲染)
fogbooleantrue是否受场景雾气影响false (穿透雾气)
alphaMapTHREE.Texturenull透明通道贴图用灰度图控制透明度
aoMapTHREE.Texturenull环境遮挡贴图模拟阴影凹陷效果
visiblebooleantrue是否可见false (隐藏材质)

本案例会用到vertexColors属性,它存在于基类Material上,用来决定是否使用顶点着色,下面是实现代码

const geometry = new THREE.BoxGeometry( 1, 1, 1); 
const att_pos = geometry.getAttribute( 'position' ); 
const data_color =[];
let i=0;
while ( i < att_pos.count ) {
    data_color.push(Math.random(),Math.random(),Math.random()); // 生成随机颜色数据
    i += 1;
}
const att_color = new THREE.BufferAttribute( new Float32Array( data_color ), 3 ); 
geometry.setAttribute( 'color', att_color ); // 设置颜色属性

const material = new THREE.MeshBasicMaterial( { vertexColors: true } ); // 启用顶点颜色
const box = new THREE.Mesh( geometry, material )
scene.add( box )

从代码可以发现,我们获取到几何体顶点位置数据,循环地点数量,生成一组随机颜色,然后设置几何体颜色属性,最后启用顶点着色实现上面案例效果

组合材质案例

假设我们现在有一个立方体,需要每个面设置不同的颜色该如何实现,先看效果图

2.gif

这里涉及到材质相关问题,查看官网中关于Mesh的说明

image.png

既然material可以为一个数组,那就好办了

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

[0,1,2,3,4,5].forEach((mi,i) => {
    geometry.groups[i].materialIndex = mi;   // 设置每个组的材质索引
})
const materials = [
    new THREE.MeshBasicMaterial( { color: 0x00ff00 } ), 
    new THREE.MeshBasicMaterial( { color: 0x0000ff } ), 
    new THREE.MeshBasicMaterial( { color: 0xffff00 } ),
    new THREE.MeshBasicMaterial( { color: 0xff00ff } ), 
    new THREE.MeshBasicMaterial( { color: 0x00ffff } ), 
    new THREE.MeshBasicMaterial( { color: 0xff0000 } ) 
]

const box = new THREE.Mesh( geometry, materials );
scene.add( box );

线条基础材质案例

上面的案例都是作用在Mesh上的,three.js提供了作用在line上的基础材质

需要了解的API

LineBasicMaterial

LineBasicMaterial 是专用于绘制线段/线框的材质,无视光照且性能高效。适用于路径可视化、辅助线、轮廓绘制等场景。

一些常用属性

属性类型默认值作用限制说明
colorTHREE.Color0xffffff线条颜色(支持16进制/RGB/颜色名)-
linewidthnumber1线宽(像素) ,但多数平台仅支持1(WebGL规范限制)实际渲染常固定为1
opacitynumber1透明度(需配合transparent:true生效)范围 [0, 1]
transparentbooleanfalse启用透明度-

LineSegment

LineSegments 是 Three.js 中用于绘制独立线段集合的对象,适合需要断开连接的线段场景(如网格线、轮廓线)

EdgesGeometry

EdgesGeometry 是 Three.js 中用于智能提取几何体边缘的工具,可将任意几何体转化为线框结构(仅保留关键边线)。

了解这些概念后,我们的代码如下

const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material_mesh = new THREE.MeshBasicMaterial( { color: 'deepskyblue' } ); 
const material_line = new THREE.LineBasicMaterial( { color: 'deeppink' });
const box = new THREE.Mesh( geometry, material_mesh );
box.add(
    new THREE.LineSegments(
        new THREE.EdgesGeometry( geometry  ),
       material_line
    )
)
scene.add( box );

效果如下

3.gif

贴图案例

效果如图

4.gif

这个案例很简单,主要是利用canvas绘制图形,通过CanvasTexture生成纹理, map属性用来贴图

const canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d');
canvas.width = 64;
canvas.height = 64;

ctx.fillStyle = 'deepskyblue';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.lineWidth = 4;
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(canvas.width/2, canvas.height/2, 20 ,0 , Math.PI * 2);
ctx.closePath();
ctx.stroke();
ctx.fill();
ctx.beginPath();
ctx.rect(0,0,canvas.width,canvas.height);
ctx.stroke();

const cube = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1), 
    new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(canvas) }));
scene.add(cube);

uv贴图案例

先看效果

5.gif

首先我们要了解什么是uv

UV是三维图形学中的通用概念,在Three.js 中用于将2D纹理精准映射到3D模型表面。可理解为:

  • U 和 V 是二维纹理的坐标系(类似平面图的X/Y轴),范围固定为 [0, 1]
  • 每个3D模型的顶点都对应一组UV坐标,定义该顶点在纹理图中的位置。

实现思路

通过canvas创建画布生成如下纹理

image.png

const CELL_SIZE = 4; // 单元格大小
const canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'); 
canvas.width = 128; 
canvas.height = 128; 
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height); // 创建线性渐变
gradient.addColorStop(0, 'red'); 
gradient.addColorStop(1, 'blue'); 
ctx.fillStyle = gradient; // 设置填充颜色为渐变
ctx.fillRect(0, 0, canvas.width, canvas.height); 

let i = 0; 
const len = CELL_SIZE * 2; 
const cellsize = canvas.width / CELL_SIZE; // 计算单元格大小32
ctx.fillStyle = 'white';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '32px arial';
while(i<len){
    const gx = i%CELL_SIZE; // 计算当前单元格的x坐标索引 0,1,2,3...
    const gy = Math.floor(i/CELL_SIZE); // 计算当前单元格的y坐标索引 0,1,2...
    const x = cellsize * gx + cellsize / 2; // 计算当前单元格的中心点x坐标
    const y = cellsize * gy + cellsize / 1.8; // 计算当前单元格的中心点y坐标
    ctx.fillText(i, x, y); 
    i++;
}

const geometry = new THREE.BoxGeometry(1,1,1); 
const texture = new THREE.CanvasTexture(canvas);
texture.magFilter = THREE.NearestFilter; // 设置纹理过滤方式
const material = new THREE.MeshBasicMaterial({map: texture});

什么,怎么生成了这样的纹理,别急,uv的作用就是用来定义顶点在纹理图中的位置,打印看看当前几何体的UV

image.png

我们的纹理将被贴在(0,0)左下角,(1,1)右上角位置,于是乎就出现了上面的情况,我们来修改UV调整显示

const setUVFace = (uv, faceIndex, cellIndex, order, gridSize) => {
    const uvData = getUVData(faceIndex, cellIndex, gridSize);
    setUVData(uv, uvData, order );
};
const getUVData = (foceIndex = 0, cellIndex = 0,gridSize = 4) => {
   const cellX = cellIndex % gridSize;  
   const cellY = Math.floor(cellIndex / gridSize);
   let di = 0;
   const uvd = 1/gridSize; // 单元格宽度
   const uvData = []; // 存储UV数据的数组
   while(di<4){
       const i = foceIndex * 4 + di; // 计算当前顶点的索引  
       const x = di % 2; // 0,1,0,1...
       const y = 1 - 1*Math.floor(di/2); // 1,0,1,0...
       const u = uvd * cellX + uvd * x; 
       const v = 1 - uvd * (cellY + 1) + y*uvd; 
       uvData.push({i,u,v}); 
       di++;
   }
   return uvData;
}

const setUVData = (uv,uvData,order) => {
    order = order || [0,1,2,3]; 
    uvData.forEach((a,di,uvData) => {
        const b = uvData[ order[di] ]; // 获取下一个顶点的UV坐标
        uv.setXY(a.i,a.u,a.v); // 设置当前顶点的UV坐标
    })
    uv.needsUpdate = true; 
}

从上面的图可以看出,我们这里需要分成4个单元格才能得到每个面有且只显示一个单独的数字

image.png

最终可以看到如下效果

image.png

发现没有,数字产生了锯齿很模糊,如何解决呢,可以通过扩大画布大小来解决

canvas.width = 1024; 
canvas.height = 1024; 

image.png

透明贴图案例

先看效果

1.gif

透明贴图在之前章节有介绍 Three.js-硬要自学系列29之专项学习透明贴图什么是透明贴图

const canvas = document.createElement('canvas'), // 创建画布元素
ctx = canvas.getContext('2d'); // 获取画布的2D上下文

canvas.width = 100; // 设置画布宽度
canvas.height = 100; // 设置画布高度
ctx.fillStyle = 'deepskyblue'; // 设置填充颜色为深天蓝色
ctx.fillRect(0, 0, canvas.width, canvas.height); // 绘制矩形
ctx.strokeStyle = 'deeppink'; // 设置边框颜色为深粉
ctx.lineWidth = 6; // 设置边框宽度为10
ctx.strokeRect(8,8, canvas.width-16, canvas.height-16); // 绘制矩形边框
const texture = new THREE.CanvasTexture(canvas); // 创建纹理

const cube = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1), // 创建立方体几何
    new THREE.MeshBasicMaterial({
        map: texture,
        transparent: true,
        opacity: 0.5,
        color: '#fff'
    }) 
)

scene.add(cube);