初探 THREE.JS 中的着色器材质并实现一个圆形的点效果

4,082 阅读5分钟

初探 THREE.JS 中的着色器材质并实现一个圆形的点效果

云图三维 连接你·创造的世界 致力于打造国内第一家集查看、建模、装配和渲染于一体的“云端CAD”协作设计平台。

应读者的要求,希望我们成立一个专业的、面向成渝地区的前端开发人员的webgl、Threejs行业QQ交流群,便于大家讨论问题。群里有研究webgl、Threejs大佬哦,欢迎大家加入!——点击链接加入群聊【three.js/webgl重庆联盟群】:jq.qq.com/?_wv=1027&k…

作者介绍

春春,云图大前端研发工程师,负责云图三维 front 端的开发工作。

正文

从标题中 初探 可以看出,本文并不是带你深入了解THREE.JS着色器材质源码各个细节的文章;写此篇文章也是想抛砖引玉,通过介绍 THREE.JS中着色器材质,以及修改THREE.JS 中的材质、实现渲染一个圆形的点的效果;最终使你对自定义材质有一个初步的了解,并能够通过自定义材质和 WebGL 技术实现一些特殊的效果。

本文用到的前置基础知识

THREE.JS 中的自定义材质类型

THREE.JS 中自定义材质类型主要有两种,原始着色器材质(RawShaderMaterial) 和 着色器材质(ShaderMaterial)

ShaderMaterial

A material rendered with custom shaders. A shader is a small program written in GLSL that runs on the GPU

RawShaderMaterial

This class works just like ShaderMaterial, except that definitions of built-in uniforms and attributes are not automatically prepended to the GLSL shader code.

ShaderMaterial

GPU 的顶点着色器单元用于处理顶点位置、顶点颜色、顶点法向量等顶点数据,片元着色器单元用于处理贴图等片元数据,一个 WebGL 程序对象包含一个顶点着色器对象和一个片元着色器对象。他们分别运行在 GPU 的顶点着色器单元和片元着色器单元。

关于 ShaderMaterial ,从源码中可以看出 ShaderMaterial 和其他材质对象类差不多,继承于 Material,是对 Material 材质对象的功能的拓展。这里主要需要关心以下三个属性,通过这三个属性再加上对 WebGL 的熟悉,你可以实现一些非常酷炫的效果

  • uniforms
  • vertexShader
  • fragmentShader

vertexShader 和 fragmentShader 都是字符串格式,一个传入顶点这色器单元一个传入片元着色器单元。uniforms 是用于传入到顶点着色器和片元着色器中的一致化的变量。顶点着色器中的 attribute 变量需要通过 Geometry 的 attributes 传入。其实 ShaderMaterial 中还有一些 THREE.JS 自动化处理的地方(详细可以去阅读 WebGLProgram.js 源码),这里就不深入讲解了,下面用一个简单的例子来介绍一下编写 ShaderMaterial 流程。

// 初始化几何体对象
const geometry = new THREE.BufferGeometry();
// 设置顶点数据
const pos = new Float32Array([0, 0, 0]);
// 设置position attribute
geometry.setAttribute('position', new THREE.BufferAttribute(pos, 3));
​
// 初始化自定义材质对象
const material = new THREE.ShaderMaterial();
// 顶点着色器代码
material.vertexShader = `
    void main() {
        // 设置点的大小为50像素
        gl_PointSize = 50.0;
        // 设置点的位置
        gl_Position = vec4(position, 1.0);
    }
`;
// 片元着色器代码
material.fragmentShader = `
    void main() {
        // 光栅化片元的颜色
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    }
`;
// 使用 THREE.Points 顶点绘制模式
const point = new THREE.Points(geometry, material);
scene.add(point);

效果如下图: point-02.png

想要一个圆形的点

细心的读者可能发现了,这个点跟我们想象的有些不太一样,它是方形的而不是我们想要的圆点,那么如何将这个方点变成圆点呢,其实很简单,只需要在片元着色器里加一行代码, 片元着色器代码变成下面这样:

// 片元着色器代码
material.fragmentShader = `
    void main() {
        if (distance(gl_PointCoord, vec2(0.5, 0.5)) > 0.5) discard;
        // 光栅化片元的颜色
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    }
`;

效果如下图: point-03.png

接下来简单解释一下这行代码 if (distance(gl_PointCoord, vec2(0.5, 0.5)) > 0.5) discard; 究竟干了什么,它的意思是计算一下当前片元位置到坐标 vec2(0.5, 0.5)的距离如果大于 0.5,就将此片元丢弃不渲染,我们可以想象一个距离原点距离为 0.5 的图形形状,不正是一个圆嘛。

ShaderMaterial 的介绍就到此为止啦,感兴趣的小伙伴可以深入去阅读一下 ShaderMaterial 的源码和官网上的经典案例。

封装一个圆形点的类

上面实现的一个圆形的点的方式还是比较简单的,而且还有蛮多问题的(感兴趣的小伙伴可以私信跟我交流),只是用于阐明实现思路;接下来会封装一个 CPoint 类用于表示这种通用的圆形的点。

import * as THREE from 'three';
​
export interface CPointParams {
    color?: number;
    size?: number;
}
​
export class CPoint extends THREE.Points {
    constructor(params?: CPointParams) {
        super();
​
        // create geometry
        const geo = new THREE.BufferGeometry();
        const positionAttr = new THREE.BufferAttribute(
            new Float32Array([0, 0, 0]),
            3
        );
        geo.setAttribute('position', positionAttr);
​
        const color = params?.color || 0xffcc00;
        const size = params?.size || 20;
​
        //material
        const material = new THREE.PointsMaterial({
            color: color,
            size: size,
            transparent: true,
            depthTest: false,
            sizeAttenuation: false,
        });
​
        material.onBeforeCompile = (shader) => {
            shader.fragmentShader = shader.fragmentShader.replace(
                'vec4 diffuseColor = vec4( diffuse, opacity );',
                `
                if (distance(gl_PointCoord, vec2(0.5, 0.5)) > 0.5) discard;
                vec4 diffuseColor = vec4( diffuse, opacity );
            `
            );
        };
​
        this.geometry = geo;
        this.material = material;
        this.renderOrder = 1;
        this.matrixAutoUpdate = false;
    }
​
    get pointMaterial(): THREE.PointsMaterial {
        return this.material as THREE.PointsMaterial;
    }
}

测试一下使用的效果

// 调试ui
const gui = new dat.GUI();
const parameters = {
    color: 0xffcc00,
    size: 15,
};
// xyz轴
const axes = new THREE.AxesHelper();
scene.add(axes);
// 初始化cpoint
const point = new CPoint({
    color: parameters.color,
    size: parameters.size,
});
scene.add(point);
// debug ui
gui.addColor(parameters, 'color').onChange(() => {
    point.pointMaterial.color.set(parameters.color);
});
gui.add(point.pointMaterial, 'size').min(1).max(100);

总结

THREE.JS 是一个很优秀的三维渲染库,其对底层 WebGL API 的封装很值得去研究,感兴趣的小伙伴可以去深入了解着色器部分封装的代码(src/renderers文件夹)。