threejs系列-图元、材质、纹理

图元

图元(primitives)就是在运行时根据大量的参数生成的 3D 形状。
应用级别的 primitives 是设计师通过一些见图然间生成的,此章中,threejs

图形类别

六面体 BoxGeometry

具体查看
BoxGeometry

解释下参数

segments

六边形上有参数 widthSegments 、heightSegments 、depthSegments ,默认值均为 1,意思是沿着长宽高的矩形面分割几下,比如 segment=1,就是切 1 下,也就是把面切成了两块,如下:
segment=1

平面圆、扇形

CircleGeometry

theta 为圆心角

radius

半径,默认为 1

thetaStart

第一个 segment 开始的角度,默认为 0(3 点钟方向)。可以理解为从哪里开始计算圆心角。

thetaLength

可以理解为,圆心角的角度。

pS1kQC8.png

锥体、柱体

关于 theta\radius 的参数设置与圆形类似,详看文档

ConeGeometry

CylinderGeometry

openEnded

一个布尔值,当前锥体是否打开,默认为 false,即封闭。

球体

SphereGeometry

这里需要注意区分 phi 和 theta。

phiLength 默认为 Math.PI * 2,也就是一个球形的水平方向的展开角度

pS1A0ot.png

而 theta 所表示的弧形,一个球形的垂直方向的展开角度。可以理解为,当前形状的是个盆、还是个碗。

2d 平面 PlaneGeometry

PlaneGeometry

这个形状很简单,只规定宽高和 segment 即可

EdgesGeometry

一个工具,用来画外部框线的(和 LineSegments 结合使用),把一个 geometry 输入进去,会输出一系列的框线

const geometry = new THREE.ConeGeometry(1, 2, 6)
const edges = new THREE.EdgesGeometry(geometry)
const line = new THREE.LineSegments(
    edges,
    new THREE.LineBasicMaterial({ color: 0xffffff })
)

return line

其他体

其他形状阅读

BufferGeometry 和 Geometry

BufferGeometry 性能要比 Geometry 更好,但用法比较麻烦,比如要创建一个简单的平面矩形,需要我们自定义顶点,如下:

const geometry = new THREE.BufferGeometry()
// 创建一个简单的矩形. 在这里我们左上和右下顶点被复制了两次。
// 因为在两个三角面片里,这两个顶点都需要被用到。
const vertices = new Float32Array([
    -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.0, -1.0, -1.0, 1.0,
])

// itemSize = 3 因为每个顶点都是一个三元组。
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3))
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })
const mesh = new THREE.Mesh(geometry, material)

而普通的 Geometry 是一系列被 three 封装好的类,其底层都是 BufferGeometry。

segment 数量与性能关系

当 segment 越多时,表面越平滑,同时,gpu 的计算量更大,占用的内存也更大。对于球体来说,需要考虑平滑的程度和应用的适用场景,选取最折中的方案。

但,对于平面来说,无论有多少 segment,区别并不大。

材质

两种设置时机

设置材质时,分为实例化时设置和实例化后设置,换句话说,一种是把参数传入构造函数时实例化 Material 对象,另一种方式是实例化后调用实力上的方法进行参数设置。

两种方法的使用时机区别于看当前 Material 对象是否需要复用和进行定制化操作。

实例化时

const material = new THREE.MeshPhongMaterial({
    color: 0xff0000, // 红色 (也可以使用CSS的颜色字符串)
    flatShading: true,
})

实例化后

const material = new THREE.MeshPhongMaterial()
material.color.setHSL(0, 1, 0.5) // 红色
material.flatShading = true

色彩

hsl 色彩模型

three 中支持 rgb 系统和 hsl 系统,对于 css 中经常使用的 rgb 色彩模型就不细说了。

hue 色相

(0,1),红色为 0,绿色为 0.66,蓝色为 0.66。hue 定一个颜色基调

saturation 饱和度

(0,1) 0 为没有颜色,1 为原色。在色彩学中,原色饱和度最高,随着饱和度降低,色彩变得暗淡直至成为无彩色,即失去色相的色彩。

luminance 明度

(0,1) 0 为黑色,1 为白色,0.5 为颜色的本色。也就是说,在明度 0 到 0.5 的时候,颜色由黑色逐渐渐变为色相原色;同理,明度由 1.0 到 0.5 时,颜色由白色逐渐渐变为色相原色。

设置方式

实例化前设置颜色:

const m1 = new THREE.MeshBasicMaterial({ color: 0xff0000 }) // 红色
const m2 = new THREE.MeshBasicMaterial({ color: 'red' }) // 红色
const m3 = new THREE.MeshBasicMaterial({ color: '#F00' }) // 红色
const m4 = new THREE.MeshBasicMaterial({ color: 'rgb(255,0,0)' }) // 红色
const m5 = new THREE.MeshBasicMaterial({ color: 'hsl(0,100%,50%)' }) // 红色

实例化后:

material.color.set(0x00ffff) // 同 CSS的 #RRGGBB 风格
material.color.set(cssString) // 任何 CSS 颜色字符串, 比如 'purple', '#F32',
// 'rgb(255, 127, 64)',
// 'hsl(180, 50%, 25%)'
material.color.set(someColor) // 其他一些 THREE.Color
material.color.setHSL(h, s, l) // 其中 h, s, 和 l 从 0 到 1
material.color.setRGB(r, g, b) // 其中 r, g, 和 b 从 0 到 1

材质类别

MeshBasicMaterial 不受光照的影响。

MeshLambertMaterial 只在顶点计算光照。

MeshPhongMaterial 则在每个像素计算光照,还支持镜面高光。

Phong 网格材质(MeshPhongMaterial)

pS3yk3n.png

PBR —— Physically Based Rendering

基于物理渲染(Physically Based Rendering)的材质,已经作为 3D 应用程序的标准。PBR 会提供比标准的MeshLambertMaterialMeshPhongMaterial更逼真的效果,但计算成本也会更高

这种方法与旧方法的不同之处在于,不使用近似值来表示光与表面的相互作用,而是使用物理上正确的模型。 我们的想法是,不是在特定照明下调整材质以使其看起来很好,而是可以创建一种材质,能够“正确”地应对所有光照场景。

标准网格材质(MeshStandardMaterial)

标准网格材质(MeshStandardMaterial)

他与 MeshPhongMaterial 在设置光泽程度的区别在于,MeshPhongMaterial 只需要设置 shininess 属性,而 MeshStandardMaterial 需要使用 roughness 和 metalness 来定义光的反射与漫反射程度。

在基本层面,roughness 是 shininess 的对立面。粗糙度(roughness)高的东西,比如棒球,就不会有很强烈的反光,而不粗糙的东西,比如台球,就很有光泽。粗糙度的范围从 0 到 1。

另一个设定,metalness,说的是材质的金属度。金属与非金属的表现不同。0 代表非金属,1 代表金属。

这里是 MeshStandardMaterial 的一个快速示例,从左至右看,粗糙度从 0 到 1,从上至下看,金属度从 0 到 1。

pS3co7T.png

roughness

材质的粗糙程度。0.0 表示平滑的镜面反射,1.0 表示完全漫反射。

(0,1)当 roughness 越接近 1,表面越粗糙,反射的光的强度越弱,漫反射能力越强,如下为 roughness=0.8,metalness=0.3

pS3gaUU.png

当 roughness=0.2 时,metalness 不变,如下
pS3gD29.png

metalness

材质与金属的相似度。非金属材质,如木材或石材,使用 0.0,金属使用 1.0,通常没有中间值。 默认值为 0.0。0.0 到 1.0 之间的值可用于生锈金属的外观。如果还提供了 metalnessMap,则两个值相乘。

当 roughness=0.5, metalness=0.1 时,材质越接近木材

pS3gqVf.png

当 roughness 相同,metalness=0.9 时,发现材质的反射能力更强
pS32iZV.png

物理网格材质(MeshPhysicalMaterial)

MeshPhysicalMaterial 与 MeshStandardMaterial 相同,但它增加了一个 clearcoat 参数,该参数从 0 到 1,决定了要涂抹的清漆光亮层的程度,还有一个 clearCoatRoughness 参数,指定光泽层的粗糙程度。

物理网格材质(MeshPhysicalMaterial)

clearcoat

范围(0,1)。当需要在表面加一层薄薄的半透明材质的时候,可以使用与 clear coat 相关的属性,默认为 0.0;值越接近 1,反射光的能力越强。

clearcoatRoughness

clear coat 层的粗糙度,由 0.0 到 1.0。 默认为 0.0。和 roughness 相似,当值越接近 1,漫反射能力越强。

材质选择

构建速度:MeshBasicMaterial < MeshLambertMaterial < MeshPhongMaterial < MeshStandardMaterial < MeshPhysicalMaterial

构建速度越慢的材质,做出的场景越逼真,但在低功率或移动设备上,你可能需要思考代码的设计,使用构建速度较快的材质。

纹理

在立方体上应用纹理

  1. 创建 TextureLoader
  2. 调用其上 load 方法,传入图像的路径 url
  3. 在材质上的 map 属性上使用
const getImageMaterial = (url) => {
    const loader = new THREE.TextureLoader()
    const material = new THREE.MeshBasicMaterial({
        map: loader.load(url),
    })
    return material
}

const material = getImageMaterial('./static/flowers.jpg')

pSJ8oes.png

设置多面纹理

materials 属性中还可传入数组,比如在 BoxGeometry 中,传入一个含有 6 种纹理的材质数组,最终呈现:(有平面的 Geometry 支持多面纹理)

const loader = new THREE.TextureLoader()

const materials = [
    new THREE.MeshBasicMaterial({
        map: loader.load('resources/images/flower-1.jpg'),
    }),
    new THREE.MeshBasicMaterial({
        map: loader.load('resources/images/flower-2.jpg'),
    }),
    new THREE.MeshBasicMaterial({
        map: loader.load('resources/images/flower-3.jpg'),
    }),
    new THREE.MeshBasicMaterial({
        map: loader.load('resources/images/flower-4.jpg'),
    }),
    new THREE.MeshBasicMaterial({
        map: loader.load('resources/images/flower-5.jpg'),
    }),
    new THREE.MeshBasicMaterial({
        map: loader.load('resources/images/flower-6.jpg'),
    }),
]
const cube = new THREE.Mesh(geometry, materials)

pSJGpwR.png

纹理加载

在使用loader.load方法加载纹理时,等待图片被 threejs 完全下载完成前,texture 都是完全透明的。

下载纹理后再渲染

我们也可使用 load 的回调函数,当纹理图片下载完成后,再将 mesh 加入到场景中。

const loader = new THREE.TextureLoader();
loader.load('resources/images/wall.jpg', (texture) => {
  const material = new THREE.MeshBasicMaterial({
    map: texture,
  });
  const cube = new THREE.Mesh(geometry, material);
  scene.add(cube);
  cubes.push(cube);  // 添加到我们要旋转的立方体数组中
}

等待多纹理加载

const loadManager = new THREE.LoadingManager()
const loader = new THREE.TextureLoader(loadManager)

const materials = [
    new THREE.MeshBasicMaterial({
        map: loader.load('resources/images/flower-1.jpg'),
    }),
    new THREE.MeshBasicMaterial({
        map: loader.load('resources/images/flower-2.jpg'),
    }),
    new THREE.MeshBasicMaterial({
        map: loader.load('resources/images/flower-3.jpg'),
    }),
    new THREE.MeshBasicMaterial({
        map: loader.load('resources/images/flower-4.jpg'),
    }),
    new THREE.MeshBasicMaterial({
        map: loader.load('resources/images/flower-5.jpg'),
    }),
    new THREE.MeshBasicMaterial({
        map: loader.load('resources/images/flower-6.jpg'),
    }),
]

loadManager.onLoad = () => {
    const cube = new THREE.Mesh(geometry, materials)
    scene.add(cube)
    cubes.push(cube) // 添加到我们要旋转的立方体数组中
}

增加加载纹理图片的进度条

LoadingManager 也有一个 onProgress 属性,我们可以设置为另一个回调来显示进度指示器。

首先,我们在 HTML 中添加一个进度条

<canvas id="c"></canvas>
<div id="loading">
    <div class="progress"><div class="progressbar"></div></div>
</div>

加上 css

#loading {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
}
#loading .progress {
    margin: 1.5em;
    border: 1px solid white;
    width: 50vw;
}
#loading .progressbar {
    margin: 2px;
    background: white;
    height: 1em;
    transform-origin: top left;
    transform: scaleX(0);
}

然后在代码中,我们将在 onProgress 回调中更新 progressbar 的比例。调用它有如下几个参数:最后加载的项目的 URL,目前加载的项目数量,以及加载的项目总数。

const loadingElem = document.querySelector('#loading')
const progressBarElem = loadingElem.querySelector('.progressbar')

loadManager.onLoad = () => {
    loadingElem.style.display = 'none'
    const cube = new THREE.Mesh(geometry, materials)
    scene.add(cube)
    cubes.push(cube) // 添加到我们要旋转的立方体数组中
}

loadManager.onProgress = (urlOfLastItemLoaded, itemsLoaded, itemsTotal) => {
    const progress = itemsLoaded / itemsTotal
    progressBarElem.style.transform = `scaleX(${progress})`
}

内存管理

纹理图片的存储大小与加载时长相关,而只压缩纹理图片的文件大小并不能节约内存,这是因为纹理文件占内存大小为:

(纹理图宽 * 纹理图高 * 4 * 1.33) Bytes

可见,内存大小与纹理图片的尺寸大小息息相关。
所以,在使用纹理图片的时候,不仅要关注文件大小,更要关注尺寸大小

Filtering & Mips

当纹理涂鸦>渲染对象,且渲染对象尺寸极小,比如 2*2 像素时,GPU 通过 mipmaps 来确定每一个像素使用哪些颜色。

Mips 是纹理的副本,每一个都是前一个 mip 的一半宽和一半高,其中的像素已经被混合以制作下一个较小的 mip。Mips 一直被创建,直到我们得到 1x1 像素的 Mip。

mips

现在,当立方体被画得很小,只有 1 或 2 个像素大时,GPU 可以选择只用最小或次小级别的 mip 来决定让小立方体变成什么颜色。

当纹理绘制的尺寸大于其原始尺寸时,你可以将 texture.magFilter 属性设置为 THREE.NearestFilter 或 THREE.LinearFilter 。

NearestFilter 意味着只需从原始纹理中选取最接近的一个像素。对于低分辨率的纹理,这给你一个非常像素化的外观,就像 Minecraft。

LinearFilter 是指从纹理中选择离我们应该选择颜色的地方最近的 4 个像素,并根据实际点与 4 个像素的距离,以适当的比例进行混合。

THREE.NearestFilter 同上,在纹理中选择最近的像素。

THREE.LinearFilter 和上面一样,从纹理中选择4个像素,然后混合它们

THREE.NearestMipmapNearestFilter 选择合适的mip,然后选择一个像素。

THREE.NearestMipmapLinearFilter 选择2个mips,从每个mips中选择一个像素,混合这2个像素。

THREE.LinearMipmapNearestFilter 选择合适的mip,然后选择4个像素并将它们混合。

THREE.LinearMipmapLinearFilter 选择2个mips,从每个mips中选择4个像素,然后将所有8个像素混合成1个像素。  (效果最好,但速度慢)

使用 mipmaps 后,远处物体会看的更清晰,且在动态场景种,远方物体不会闪烁,渲染效果更好。

纹理重复、偏移、旋转、包裹

具体查看:在线示例

一个建镜像花纹的 tips

将包裹方式全部变为镜像重复MirroredRepeatWrapping,之后设置重复倍数

texture.wrapS = THREE.MirroredRepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.x = 4
texture.repeat.y = 4

reference

  1. 官方文档-material
  2. 官方文档-textures