Three.js-硬要自学系列39之专项学习缓冲几何体颜色属性

180 阅读5分钟

前言

好久不见,感谢大家一直以来的关注,最近因工作繁忙,断更好久,今天忙里抽闲继续写点我对three.js的学习心得,内容不是很多,我尽量控制在10分钟内

颜色属性是指什么

颜色属性就是指给缓冲几何体中的每个点(顶点)分配颜色的设定。在 three.js 中,它通常叫作color attribute。每个点都有自己的颜色值(比如红色、蓝色),这些值组合起来,就能让整个模型呈现出丰富的色彩效果。

通俗点说,假设我们有一个球体模型,它由 100 个小点组成。颜色属性就像给每个点贴上一个小色卡(如“点1是红色,点2是蓝色”)。渲染时,这些颜色会平滑过渡,让球体看起来更真实(而不是单一颜色)。

颜色属性它被存储为一个数字数组,每组数字代表一个点的颜色(通常用 RGB 值,比如 [1, 0, 0] 表示纯红色)。计算机显卡直接读取这个数组,高效绘制颜色。

那么它是如何进行工作的

当 3D 模型显示在屏幕上时,显卡会根据每个点的颜色值进行“插值”(简单说,就是自动混合相邻点的颜色),生成平滑的渐变效果。比如,如果点的颜色从红变蓝,模型表面就会出现彩虹般的过渡。我们可以随时修改颜色数组来改变整个模型的外观(比如动态切换颜色),不需要重建几何体

来看一个简单的例子,可能就好比较好理解

案例 - Color颜色

创建一个球形几何体,获取它的顶点数量,并创建一个颜色数组,为其添加顶点颜色值,利用BufferAttribute生成颜色属性给到几何体,注意材质那块必须设置vertexColors: true 启用顶点颜色功能,这样我们就创建生成了一个渐变的球体

效果如下

1.gif

const geo = new THREE.SphereGeometry(1,64,64);
const len = geo.getAttribute('position').count;
const color_array = [];
let i = 0;
while(i < len) {
    const a1 = i / len;
    const a2 = 1 - Math.abs(0.5 - a1) / 0.5;
    color_array.push(0,a2, 1-a2);
    i++;
}

const color_attribute = new THREE.BufferAttribute(new Float32Array(color_array), 3); 
geo.setAttribute('color', color_attribute);

const material1 = new THREE.MeshBasicMaterial({vertexColors: true});

const mesh1 = new THREE.Mesh(geo, material1);
scene.add(mesh1);

案例 - materails材质影响

2.gif

这个案例演示了不同材质类型如何处理顶点颜色,界面上我创建了9个小球,分别为其给予不同的材质

  • Basic(基础)
  • Depth(深度)
  • Lambert(兰伯特)
  • Matcap(材质捕获)
  • Normal(法线)
  • Phong(Phong着色)
  • Physical(物理)
  • Standard(标准)
  • Toon(卡通)
const geo = new THREE.SphereGeometry( 0.5, 60, 60 );
const len = geo.getAttribute('position').count;
const color_array = [];
let i = 0;
while(i < len){
    const a1 = i / len;
    const a2 = 1 - Math.abs(0.5 - a1) / 0.5;
    color_array.push(0, a2, 1 - a2);
    i += 1;
}
const color_attribute = new THREE.BufferAttribute(new Float32Array(color_array), 3);
geo.setAttribute('color', color_attribute)

const meshOpt = {
    vertexColors: true
}

'Basic,Depth,Lambert,Matcap,Normal,Phong,Physical,Standard,Toon'.split(',').forEach( (matStr, i) => {
    const x = i % 5;
    const y = Math.floor(i / 5);
    const material = new THREE['Mesh' + matStr + 'Material'](meshOpt);
    const mesh = new THREE.Mesh(geo, material);
    mesh.position.set(0.5 + x, 0, 0.5 + y)
    scene.add(mesh);
});

案例 - lines线条

这个案例很简单,这里只是使用线框模式渲染几何体,并为线条的每个顶点应用渐变颜色,只是单纯的验证下three.js中线条材质对顶点颜色的支持,看效果

3.gif

const geo = new THREE.SphereGeometry( 1, 10, 10 );
const len = geo.getAttribute('position').count;
const color_array = [];
let i = 0;
while(i < len){
const a1 = i / len;
const a2 = 1 - Math.abs(0.5 - a1) / 0.5;
color_array.push(0, a2, 1 - a2);
i += 1;
}
const color_attribute = new THREE.BufferAttribute(new Float32Array(color_array), 3);
geo.setAttribute('color', color_attribute)

const material = new THREE.LineBasicMaterial({ vertexColors: true, linewidth: 6 });
const line = new THREE.Line(geo, material);
scene.add(line);

案例 - points点

这个案例同上面的线条案例差不多,这里使用的是点模式渲染几何体

4.gif

const geo = new THREE.BoxGeometry( 2, 2, 2, 8, 8, 8 );
const len = geo.getAttribute('position').count;
const color_array = [];
let i = 0;
while(i < len){
    const a1 = i / len;
    const a2 = 1 - Math.abs(0.5 - a1) / 0.5;
    color_array.push(0, a2, 1 - a2);
    i += 1;
}
const color_attribute = new THREE.BufferAttribute(new Float32Array(color_array), 3);
geo.setAttribute('color', color_attribute)

const material = new THREE.PointsMaterial({ vertexColors: true, size: 0.05 });
const points = new THREE.Points(geo, material);
scene.add(points);

案例 - 循环改变颜色

先看效果

5.gif

从图中可以看见,我创建了一个带有动态颜色变化的环形几何体,它循环往复的跟新顶点颜色属性来实现流动的色彩变化

核心代码

const update_color_attribute = (geo, a1, a2, a3) => {
    const color_array = make_color_array(geo, a1, a2, a3);
    const color_attribute = geo.getAttribute('color');
    if(color_attribute){
        color_attribute.array = color_attribute.array.map((n, i) => {
            return color_array[i];
        });
        color_attribute.needsUpdate = true;
    }else{
        const new_color_attribute = new THREE.BufferAttribute(new Float32Array(color_array), 3);
        geo.setAttribute('color', new_color_attribute);
    }
};

它用于动态更新几何体颜色属性,如果颜色属性存在,则更新其数组内容并标记需要更新,否则创建新的颜色属性并给到几何体

完整代码如下

const make_color_array = (geo, a1, a2, a3) => {
    a1 = a1 === undefined ? 1 : a1;
    a2 = a2 === undefined ? 1 : a2;
    a3 = a3 === undefined ? 1 : a3;
    const len = geo.getAttribute('position').count;
    const color_array = [];
    let i = 0;
    while(i < len){
        const a_index = i / len;
        const a_indexbias = 1 - Math.abs(0.5 - a_index) / 0.5;
        color_array.push(a1, a_indexbias * a2, 1 - a_indexbias * a3);
        i += 1;
    }
    return color_array;
};

const update_color_attribute = (geo, a1, a2, a3) => {
    const color_array = make_color_array(geo, a1, a2, a3);
    const color_attribute = geo.getAttribute('color');
    if(color_attribute){
        color_attribute.array = color_attribute.array.map((n, i) => {
            return color_array[i];
        });
        color_attribute.needsUpdate = true;
    }else{
        const new_color_attribute = new THREE.BufferAttribute(new Float32Array(color_array), 3);
        geo.setAttribute('color', new_color_attribute);
    }
};

const getBias = (a1, count) => {
    return 1 - Math.abs(0.5 - a1 * count % 1) / 0.5;
};

const geo = new THREE.TorusGeometry( 1.4, 0.6, 40, 40 );
geo.rotateX(Math.PI * 0.55)
update_color_attribute(geo, 1, 1, 1);
const material1 = new THREE.MeshPhongMaterial({
    vertexColors: true,
    shininess: 15, // 光泽度
    specular: new THREE.Color(0.75, 0.75, 0.75) // 高光颜色
});
const mesh1 = new THREE.Mesh(geo, material1);
scene.add(mesh1);

const dl = new THREE.DirectionalLight(0xffffff, 0.95);
dl.position.set(5, 3, 1);
scene.add(dl);
const al = new THREE.AmbientLight(0xffffff, 0.05);
scene.add(al);

const FPS_UPDATE = 30, 
FPS_MOVEMENT = 30,  
FRAME_MAX = 300;
let secs = 0,
frame = 0,
lt = new Date();
const update = function(frame, frameMax){
    const a1 = frame / frameMax;
    const a2 = getBias(a1, 1);
    const a3 = getBias(a1, 8);
    const a4 = getBias(a1, 16);
    update_color_attribute(geo, a2, a3, a4);
};

orbitControls.update();

function animation() {
    const now = new Date(),
    secs = (now - lt) / 1000;

    requestAnimationFrame( animation );
    if(secs > 1 / FPS_UPDATE){
        update( Math.floor(frame), FRAME_MAX);
        renderer.render(scene, camera);
        // step frame
        frame += FPS_MOVEMENT * secs;
        frame %= FRAME_MAX;
        lt = now;
    }
}
animation();