【 Threejs 】- Shader 着色器实例渲染教程

着色器在threejs中是一个难点,话不多说,先来看看着色器是什么?

chrome-capture-2022-11-8.gif

一本书上是这么描述的:

如果您已经有使用计算机绘图的经验,您就会知道在这个过程中您先画一个圆,然后画一个矩形、一条线、一些三角形,直到您组成您想要的图像。这个过程与手写一封信或一本书非常相似——它是一组指令,一个接一个地完成任务。
着色器也是一组指令,但这些指令是针对屏幕上的每个像素一次性执行的。这意味着您编写的代码必须根据像素在屏幕上的位置而有所不同。就像打字机一样,您的程序将作为接收位置并返回颜色的函数工作,并且在编译时运行速度非常快。

着色器的来源

那么好,说人话,着色器到底是什么?字面意思,给物品上色的(简单^_^)

关于着色器(shader) 它是用GLSL(着色器语言)写的,执行shader的是GPU,我们可以把一部分GLSL的工作交给GPU来提升性能。这样就好理解了,着色器程序通常在计算机的图形处理单元 (GPU) 上运行,它们可以在其中并行运行。

GLSL官方解释为 :OpenGL Shading Language 也称作 GLslang,是一个以C语言为基础的高阶着色语言。它是由 OpenGL ARB 所建立,提供开发者对绘图管线更多的直接控制,而无需使用汇编语言或硬件规格语言。

  • 有些人就会问了,为什么使用着色器?
  • 答:因为它快啊!

为了回答这个问题,我介绍了并行处理的奇迹。

将你的计算机的 CPU 想象成一个大型工业管道,每项任务都是通过它的东西 – 就像一条工厂生产线。有些任务比其他任务更大,这意味着它们需要更多的时间和精力来处理。由于计算机的体系结构,作业被迫连续运行;每项工作必须一次完成一项。现代计算机通常有四个处理器组,它们像这些管道一样工作,一个接一个地完成任务以保持运行顺畅。每个管道也称为线程

中央处理器

视频游戏和其他图形应用程序比其他程序需要更多的处理能力。由于它们的图形内容,它们必须进行大量的逐像素操作。屏幕上的每个像素都需要计算,在 3D 游戏中,几何和透视也需要计算。

让我们回到管道和任务的比喻。屏幕上的每个像素代表一个简单的小任务。单独每个像素任务对 CPU 来说不是问题,但是(这就是问题所在)必须对屏幕上的每个像素完成小任务!这意味着在旧的 800×600 屏幕中,每帧必须处理 480,000 个像素,这意味着每秒要进行 14,400,000 次计算!是的!这是一个足以使微处理器过载的问题。在以每秒 60 帧的速度运行的现代 2880×1800 视网膜显示器中,该计算加起来达到每秒 311,040,000 次计算。图形工程师如何解决这个问题?

这是并行处理成为一个很好的解决方案的时候。与其拥有几个大而强大的微处理器或管道,不如让许多微型微处理器同时并行运行更聪明。这就是图形处理器单元 (GPU)。

显卡

将微型微处理器想象成一张管道表,将每个像素的数据想象成一个乒乓球。每秒 14,400,000 个乒乓球几乎可以阻塞任何管道。但是每秒接收 30 个 480,000 像素波的 800×600 微型管道表可以顺利处理。这在更高的分辨率下同样有效——你拥有的并行硬件越多,它可以管理的流就越大。

GPU 的另一个“超能力”是通过硬件加速的特殊数学函数,因此复杂的数学运算直接由微芯片而不是软件来解决。这意味着超快的三角函数和矩阵运算 – 与电流一样快。

痛并快乐着

下面将从一个实例来介绍着色器的使用,听咱给你细细道来~
着色器的功能和拓展很丰富,如果你对UI和一些图形学感兴趣,那么你学shader就很容易上手,但对我们一些前端学习者们,依旧要迎难而上。不过首先你得对threejs有一定的了解,当然你可以参考我的上一篇文章里面也有对threejs的分享和一些整合:https://juejin.cn/post/7166526980152623117

chrome-capture-2022-11-8.gif
那么咱们就开始动手一探究竟!

案例分析

首先拿到图片,给我了三个信号:会动的 有颜色的 立体盒子,旋转好说,立体盒子更好说,有颜色嘛?好像也不难,那岂不是很easy!?

Action:

  • 在工作区新建一个文件夹 threejs_box 并cd进入该文件夹
  • npm init -y + npm i parcel-bundler + npm i three 初始化 并安装依赖
  • 在该文件夹下新建src文件下 并创建 main.js 和 index.html 文件
  • 最后在生成的package文件中的”scripts”里把原有的替换为
    "dev": "parcel src/index.html",
    "build": "parcel build src/index.html"

这样一个基础框架就搭好了
接下来index.html里

<body>
    <div id="container"></div>
    <script src="./main.js" type="module"></script>
</body>

main.js中
创建threejs的三要素摄像机,场景,渲染器,以及加一个clock时钟。

import * as THREE from 'three';
let camera, scene, renderer, clock;
init();
animate();
function init() {
    // 绑定到DOM
    const container = document.getElementById( 'container' );
    // 创建摄像机
    camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 3000 );
    camera.position.z = 4;
    // 创建场景
    scene = new THREE.Scene();
    // 创建时钟
    clock = new THREE.Clock();
    // 创建渲染器
    renderer = new THREE.WebGLRenderer();
    renderer.setPixelRatio( window.devicePixelRatio );
    container.appendChild( renderer.domElement );
    onWindowResize();
    window.addEventListener( 'resize', onWindowResize );
}
// 自适应屏幕
function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize( window.innerWidth, window.innerHeight );
}
function animate() {
    requestAnimationFrame( animate );
    render();
}
function render() {
    renderer.render( scene, camera );
}

接下来要引入着色器的使用

着色器的分类

shaders有两种,一个是vertex shaders(顶点着色器),另一个是fragment shaders(片元着色器)

顶点着色器

这样的script标签,浏览器不能识别,所以不会执行,我们需要通过threejs来执行

  • uniform float time , uniform变量可以在顶点着色器和片元着色器中共同使用,time 变量在运行的时候,是以毫秒为单位。
  • varing vec2 vUv , varing 变量是顶点着色器和片元着色器的接口,这里的vUv保存的是UV映射,存储每个顶点的位置关系,我们可以使用vUv在片元着色器中。
  • void main 函数,所有的着色器必须要有这个函数,在函数中可以把uv映射进行转换(转换顶点位置),最后,我们使用gl_position来实际改变每个顶点的位置,这个改变位置的通用公式是projectionMatrix * modelViewMatrix * vec4(newPosition , 1.0),使用乘积,如果没有这一步,GPU将不会识别我们对UV进行的操作(不会修改UV的位置)

片元着色器

前两个变量是不变的,需要记住的是,所有要用到的变量,都要写在每个着色器中(片元和顶点)

  • 在main函数中,我们通过uv和time 来计算颜色(颜色必须是正的)
  • gl_FragColor 来设置片元的颜色。
  • 这里只是设置好了顶点着色器和片元着色器,还需要在threejs中结合shaderMaterial材质进行设计

THree.shaderMaterial

先定义一个uniforms变量(只是个对象结构的变量)

定义shaderMaterial材质,使用了uniforms变量,还有两个定义的着色器!

  • 为什么要定义这个unifroms变量? 用于修改uv(顶点的位置),这里定义了time,修改time也是同样的效果。

使用总结

  1. 顶点着色器和片元着色器需要保持变量统一,uniform变量和varying变量
  2. 顶点着色器和片元着色器的都需要main函数
  3. 顶点着色器的实际修改是gl_Position,片元着色器的实际修改是gl_FragColor
  4. 浏览器不执行着色器语言,需要借助Threejs
  5. Threejs使用ShaderMaterial来使用着色器,可以通过变量间接修改着色器内容!
  6. 着色器是通过GPU渲染的,不会占用CPU资源

好了接下来所学即所用,把这些枯燥的知识运用到代码当中。

  • main.js 完整代码
import * as THREE from 'three';

let camera, scene, renderer, clock;
let uniforms1,uniforms2;
init();
animate();

    function init() {
        const container = document.getElementById( 'container' );
        camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 3000 );
        camera.position.z = 4;
        scene = new THREE.Scene();
        clock = new THREE.Clock();
        // 构建基本的立体框大小
        const geometry = new THREE.BoxGeometry( 0.75, 0.75, 0.75 );
        // 创建第一个变量
        uniforms1 = {
            'time': { value: 1.0 }
        };
        // 创建第二个变量
        uniforms2 = {
            'time': { value: 1.0 },
            // 第二个变量与第一个不同的就在这里,添加了图片贴图
            'colorTexture': { value: new THREE.TextureLoader().load( '../texture/disturb.jpeg' ) }
        };
        // 把他的样式重复并且对称
        uniforms2[ 'colorTexture' ].value.wrapS = uniforms2[ 'colorTexture' ].value.wrapT = THREE.RepeatWrapping;
        // 创建数组来保存这四个立体框
        const params = [
            [ 'fragment_shader1', uniforms1 ],
            [ 'fragment_shader2', uniforms2 ],
            [ 'fragment_shader3', uniforms1 ],
            [ 'fragment_shader4', uniforms1 ]
        ];
        // 循环输出 并渲染到页面
        for ( let i = 0; i < params.length; i ++ ) {
            // geometry已经在前面确定了 这里只需要定material就行了
            const material = new THREE.ShaderMaterial( {
                // 定义的uniforms 用于修改顶点位置
                uniforms: params[ i ][ 1 ],
                // 顶点着色器 绑定
                vertexShader: document.getElementById( 'vertexShader' ).textContent,
                // 片元着色器绑定
                fragmentShader: document.getElementById( params[ i ][ 0 ] ).textContent

            } );
            // 绑定geometry 和 material
            const mesh = new THREE.Mesh( geometry, material );
            // 设置位置
            mesh.position.x = i - ( params.length - 1 ) / 2;
            mesh.position.y = i % 2 - 0.5;
            // 添加到场景
            scene.add( mesh );

        }

        renderer = new THREE.WebGLRenderer();
        renderer.setPixelRatio( window.devicePixelRatio );
        container.appendChild( renderer.domElement );

        onWindowResize();

        window.addEventListener( 'resize', onWindowResize );

    }

    function onWindowResize() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize( window.innerWidth, window.innerHeight );

    }

    function animate() {
        requestAnimationFrame( animate );
        render();
    }

    function render() {
        // 设置旋转周期时间 - 获得前后两次执行该方法的时间间隔
        const delta = clock.getDelta();
        uniforms1[ 'time' ].value += delta * 5;
        uniforms2[ 'time' ].value = clock.elapsedTime;
        for ( let i = 0; i < scene.children.length; i ++ ) {
            const object = scene.children[ i ];
            object.rotation.y += delta * 0.5 * ( i % 2 ? 1 : - 1 );
            object.rotation.x += delta * 0.5 * ( i % 2 ? - 1 : 1 );
        }
        // 将场景和摄像机渲染到页面
        renderer.render( scene, camera );

    }
  • index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - shader [Monjori]</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
</head>
<body>

<div id="container"></div>
<script id="fragment_shader4" type="x-shader/x-fragment">
                    uniform float time;
                    varying vec2 vUv;
                    void main( void ) {
vec2 position = - 1.0 + 2.0 * vUv;
float red = abs( sin( position.x * position.y + time / 5.0 ) );
float green = abs( sin( position.x * position.y + time / 4.0 ) );
float blue = abs( sin( position.x * position.y + time / 3.0 ) );
gl_FragColor = vec4( red, green, blue, 1.0 );
                    }
</script>

<script id="fragment_shader3" type="x-shader/x-fragment">
uniform float time;
varying vec2 vUv;
void main( void ) {
                            vec2 position = vUv;
                            float color = 0.0;
                            color += sin( position.x * cos( time / 15.0 ) * 80.0 ) + cos( position.y * cos( time / 15.0 ) * 10.0 );
                            color += sin( position.y * sin( time / 10.0 ) * 40.0 ) + cos( position.x * sin( time / 25.0 ) * 40.0 );
                            color += sin( position.x * sin( time / 5.0 ) * 10.0 ) + sin( position.y * sin( time / 35.0 ) * 80.0 );
                            color *= sin( time / 10.0 ) * 0.5;
                            gl_FragColor = vec4( vec3( color, color * 0.5, sin( color + time / 3.0 ) * 0.75 ), 1.0 );
}

</script>

<script id="fragment_shader2" type="x-shader/x-fragment">
uniform float time;
uniform sampler2D colorTexture;
varying vec2 vUv;
void main( void ) {
                            vec2 position = - 1.0 + 2.0 * vUv;
                            float a = atan( position.y, position.x );
                            float r = sqrt( dot( position, position ) );
                            vec2 uv;
                            uv.x = cos( a ) / r;
                            uv.y = sin( a ) / r;
                            uv /= 10.0;
                            uv += time * 0.05;
                            vec3 color = texture2D( colorTexture, uv ).rgb;
                            gl_FragColor = vec4( color * r * 1.5, 1.0 );
}
</script>

<script id="fragment_shader1" type="x-shader/x-fragment">
uniform float time;
varying vec2 vUv;
void main(void) {
                            vec2 p = - 1.0 + 2.0 * vUv;
                            float a = time * 40.0;
                            float d, e, f, g = 1.0 / 40.0 ,h ,i ,r ,q;
                            e = 400.0 * ( p.x * 0.5 + 0.5 );
                            f = 400.0 * ( p.y * 0.5 + 0.5 );
                            i = 200.0 + sin( e * g + a / 150.0 ) * 20.0;
                            d = 200.0 + cos( f * g / 2.0 ) * 18.0 + cos( e * g ) * 7.0;
                            r = sqrt( pow( abs( i - e ), 2.0 ) + pow( abs( d - f ), 2.0 ) );
                            q = f / r;
                            e = ( r * cos( q ) ) - a / 2.0;
                            f = ( r * sin( q ) ) - a / 2.0;
                            d = sin( e * g ) * 176.0 + sin( e * g ) * 164.0 + r;
                            h = ( ( f + d ) + a / 2.0 ) * g;
                            i = cos( h + r * p.x / 1.3 ) * ( e + e + a ) + cos( q * g * 6.0 ) * ( r + h / 3.0 );
                            h = sin( f * g ) * 144.0 - sin( e * g ) * 212.0 * p.x;
                            h = ( h + ( f - e ) * q + sin( r - ( a + h ) / 7.0 ) * 10.0 + i / 4.0 ) * g;
                            i += cos( h * 2.3 * sin( a / 350.0 - q ) ) * 184.0 * sin( q - ( r * 4.3 + a / 12.0 ) * g ) + tan( r * g + h ) * 184.0 * cos( r * g + h );
                            i = mod( i / 5.6, 256.0 ) / 64.0;
                            if ( i < 0.0 ) i += 4.0;
                            if ( i >= 2.0 ) i = 4.0 - i;
                            d = r / 350.0;
                            d += sin( d * d * 8.0 ) * 0.52;
                            f = ( sin( a * g ) + 1.0 ) / 2.0;
                            gl_FragColor = vec4( vec3( f * i / 1.6, i / 2.0 + d / 13.0, i ) * d * p.x + vec3( i / 1.3 + d / 8.0, i / 2.0 + d / 18.0, i ) * d * ( 1.0 - p.x ), 1.0 );
}

</script>

<script id="vertexShader" type="x-shader/x-vertex">
varying vec2 vUv;
void main(){
                            vUv = uv;
                            vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
                            gl_Position = projectionMatrix * mvPosition;
                    }
</script>

<script src="./main.js" type="module"></script>

</body>
</html>

参考

threejs官网: https://threejs.org/docs/index.html?q=shader#api/en/materials/ShaderMaterial
shader-wikipedia: https://en.wikipedia.org/wiki/Shader

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容