着色器在threejs中是一个难点,话不多说,先来看看着色器是什么?
如果您已经有使用计算机绘图的经验,您就会知道在这个过程中您先画一个圆,然后画一个矩形、一条线、一些三角形,直到您组成您想要的图像。这个过程与手写一封信或一本书非常相似——它是一组指令,一个接一个地完成任务。> 着色器也是一组指令,但这些指令是针对屏幕上的每个像素一次性执行的。这意味着您编写的代码必须根据像素在屏幕上的位置而有所不同。就像打字机一样,您的程序将作为接收位置并返回颜色的函数工作,并且在编译时运行速度非常快。
着色器的来源
那么好,说人话,着色器到底是什么?字面意思,给物品上色的(简单_)
关于着色器(shader) 它是用GLSL(着色器语言)写的,执行shader的是GPU,我们可以把一部分GLSL的工作交给GPU来提升性能。这样就好理解了,着色器程序通常在计算机的图形处理单元 (GPU) 上运行,它们可以在其中并行运行。
GLSL官方解释为 :OpenGL Shading Language 也称作 GLslang,是一个以C语言为基础的高阶着色语言。它是由 OpenGL ARB 所建立,提供开发者对绘图管线更多的直接控制,而无需使用汇编语言或硬件规格语言。
- 有些人就会问了,为什么使用着色器?
- 答:因为它快啊!
为了回答这个问题,我介绍了并行处理的奇迹。
将你的计算机的 CPU 想象成一个大型工业管道,每项任务都是通过它的东西 - 就像一条工厂生产线。有些任务比其他任务更大,这意味着它们需要更多的时间和精力来处理。由于计算机的体系结构,作业被迫连续运行;每项工作必须一次完成一项。现代计算机通常有四个处理器组,它们像这些管道一样工作,一个接一个地完成任务以保持运行顺畅。每个管道也称为线程。
视频游戏和其他图形应用程序比其他程序需要更多的处理能力。由于它们的图形内容,它们必须进行大量的逐像素操作。屏幕上的每个像素都需要计算,在 3D 游戏中,几何和透视也需要计算。
让我们回到管道和任务的比喻。屏幕上的每个像素代表一个简单的小任务。单独每个像素任务对 CPU 来说不是问题,但是(这就是问题所在)必须对屏幕上的每个像素完成小任务!这意味着在旧的 800x600 屏幕中,每帧必须处理 480,000 个像素,这意味着每秒要进行 14,400,000 次计算!是的!这是一个足以使微处理器过载的问题。在以每秒 60 帧的速度运行的现代 2880x1800 视网膜显示器中,该计算加起来达到每秒 311,040,000 次计算。图形工程师如何解决这个问题?
这是并行处理成为一个很好的解决方案的时候。与其拥有几个大而强大的微处理器或_管道_,不如让许多微型微处理器同时并行运行更聪明。这就是图形处理器单元 (GPU)。
将微型微处理器想象成一张管道表,将每个像素的数据想象成一个乒乓球。每秒 14,400,000 个乒乓球几乎可以阻塞任何管道。但是每秒接收 30 个 480,000 像素波的 800x600 微型管道表可以顺利处理。这在更高的分辨率下同样有效——你拥有的并行硬件越多,它可以管理的流就越大。
GPU 的另一个“超能力”是通过硬件加速的特殊数学函数,因此复杂的数学运算直接由微芯片而不是软件来解决。这意味着超快的三角函数和矩阵运算 - 与电流一样快。
痛并快乐着
下面将从一个实例来介绍着色器的使用,听咱给你细细道来~ 着色器的功能和拓展很丰富,如果你对UI和一些图形学感兴趣,那么你学shader就很容易上手,但对我们一些前端学习者们,依旧要迎难而上。不过首先你得对threejs有一定的了解,当然你可以参考我的上一篇文章里面也有对threejs的分享和一些整合:juejin.cn/post/716652…
案例分析
首先拿到图片,给我了三个信号:会动的 有颜色的 立体盒子,旋转好说,立体盒子更好说,有颜色嘛?好像也不难,那岂不是很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() {// 绑定到DOMconst 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 和 materialconst 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>
最后
最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。
有需要的小伙伴,可以点击下方卡片领取,无偿分享