今天我们将要介绍的高级材质叫 THREE.ShaderMaterial (我把它翻译成自定义着色器材质)。它是 three.js 库中功能最为丰富、也是最为复杂的一种高级材质。通过它,可以定义自己的着色器,直接在 WebGL 环境中运行。着色器可以将 three.js 中的 JavaScript 对象转换为屏幕上的像素。通过这些自定义的着色器,你可以明确指定你的对象如何渲染及遮盖,或者修改 three.js 库中的默认值。
但我们本篇将不会涉及如何定制着色器的细节,这已经属于另一个专门的技术体系叫“GLSL 着色器语言”,有兴趣的朋友可以专门买一本这方面的书去仔细研究。接下来我们先看看 ShaderMaterial 可以设置的几个常用属性,大部分跟其他基础材质类似,有 wireframe、wireframeLinewidth、flatShading、fog、vertexColors 等。
特别提一下vertexColors 属性,我们可以通过它为每一个顶点定义不同的颜色。它在 CanvasRenderer 下不起作用,只在 WebGLRenderer 下起作用。关于此属性可以参考后面提到的关于 LineBasicMaterial 的例子。
出上面提到的这些属性外,ShaderMaterial 还有几个特别的属性,使用它们你可以传入数据,定制你的着色器。但它们看起来不好理解,需要结合“GLSL 着色器语言”的相关知识,如下表所示:
属性 | 描述 |
---|---|
fragmentShader (像素着色器) | 这个着色器定义的是每个传入的像素的颜色 |
vertexShader (顶点着色器) | 这个着色器允许你修改每一个传入的顶点的位置 |
uniforms (统一值) | 通过这个属性可以向你的着色器发信息。同样的信息会发到每一个顶点和片段 |
defines | 这个属性的值可以转成 vertexShader 和 fragmentShader 里的 #define 代码。该属性可以用来设置着色器程序里的一些全局变量 |
attributes | 这个属性可以修改每个顶点和片段。通常用来传递位置数据与法向量相关的数据。如果要用这个属性,那么你要为几何体中的所有顶点提供信息 |
lights | 这个属性定义光照数据是否传递给着色器。默认值是 false |
其中,最重要的部分就是:如果想要使用 ShaderMaterial 材质,就必须传入两个不同的着色器:
- vertexShader:它会在几何体的每一个顶点上执行。可以用这个着色器通过改变顶点的位置来对几何体进行变换;
- fragmentShader:它会在几何体的每一个像素上执行。在 vertexShader 里,我们会返回这个特定像素应该显示的颜色;
到目前为止,我们讨论过的所有材质,three.js 库都会提供相应的 vertexShader 和 fragmentShader ,不必我们自己去定义。
接下来,我们将要给出的这个示例,其中会创建一种动态材质,里面会用到较简单的 vertexShader,用来修改一个方块各个顶点的 x、y、z 坐标。还会用到另一个 fragmentShader(网站 http://glslsandbox.com/ 提供了很多着色器),用来创建连续变化的材质。完整代码如下:
<!DOCTYPE html> <html> <head> <title>示例 04.08 - ShaderMaterial</title> <script src="../build/three.js"></script> <script src="../build/js/controls/OrbitControls.js"></script> <script src="../build/js/libs/stats.min.js"></script> <script src="../build/js/libs/dat.gui.min.js"></script> <script src="../jquery/jquery-3.2.1.min.js"></script> <style> body { /* 设置 margin 为 0,并且 overflow 为 hidden,来完成页面样式 */ margin: 0; overflow: hidden; } /* 统计对象的样式 */ #Stats-output { position: absolute; left: 0px; top: 0px; } </style> </head> <body> <!-- 用于 WebGL 输出的 Div --> <div id="webgl-output"></div> <!-- 用于统计 FPS 输出的 Div --> <div id="stats-output"></div> <script id="vertex-shader" type="x-shader/x-vertex"> uniform float time; varying vec2 vUv; void main() { vec3 posChanged = position; posChanged.x = posChanged.x*(abs(sin(time*1.0))); posChanged.y = posChanged.y*(abs(cos(time*1.0))); posChanged.z = posChanged.z*(abs(sin(time*1.0))); gl_Position = projectionMatrix * modelViewMatrix * vec4(position*(abs(sin(time)/2.0)+0.5),1.0); //gl_Position = projectionMatrix * modelViewMatrix * vec4(posChanged,1.0); } </script> <script id="fragment-shader-1" type="x-shader/x-fragment"> precision highp float; uniform float time; uniform float alpha; uniform vec2 resolution; varying vec2 vUv; void main2(void) { vec2 position = vUv; float red = 1.0; float green = 0.25 + sin(time) * 0.25; float blue = 0.0; vec3 rgb = vec3(red, green, blue); vec4 color = vec4(rgb, alpha); gl_FragColor = color; } #define PI 3.14159 #define TWO_PI (PI*2.0) #define N 68.5 void main(void) { vec2 center = (gl_FragCoord.xy); center.x=-10.12*sin(time/200.0); center.y=-10.12*cos(time/200.0); vec2 v = (gl_FragCoord.xy - resolution/20.0) / min(resolution.y,resolution.x) * 15.0; v.x=v.x-10.0; v.y=v.y-200.0; float col = 0.0; for(float i = 0.0; i < N; i++) { float a = i * (TWO_PI/N) * 61.95; col += cos(TWO_PI*(v.y * cos(a) + v.x * sin(a) + sin(time*0.004)*100.0 )); } col /= 5.0; gl_FragColor = vec4(col*1.0, -col*1.0,-col*4.0, 1.0); } </script> <script id="fragment-shader-2" type="x-shader/x-fragment"> // from http://glsl.heroku.com/e#7906.0 uniform float time; uniform vec2 resolution; // 2013-03-30 by @hintz #define CGFloat float #define M_PI 3.14159265359 vec3 hsvtorgb(float h, float s, float v) { float c = v * s; h = mod((h * 6.0), 6.0); float x = c * (1.0 - abs(mod(h, 2.0) - 1.0)); vec3 color; if (0.0 <= h && h < 1.0) { color = vec3(c, x, 0.0); } else if (1.0 <= h && h < 2.0) { color = vec3(x, c, 0.0); } else if (2.0 <= h && h < 3.0) { color = vec3(0.0, c, x); } else if (3.0 <= h && h < 4.0) { color = vec3(0.0, x, c); } else if (4.0 <= h && h < 5.0) { color = vec3(x, 0.0, c); } else if (5.0 <= h && h < 6.0) { color = vec3(c, 0.0, x); } else { color = vec3(0.0); } color += v - c; return color; } void main(void) { vec2 position = (gl_FragCoord.xy - 0.5 * resolution) / resolution.y; float x = position.x; float y = position.y; CGFloat a = atan(x, y); CGFloat d = sqrt(x*x+y*y); CGFloat d0 = 0.5*(sin(d-time)+1.5)*d; CGFloat d1 = 5.0; CGFloat u = mod(a*d1+sin(d*10.0+time), M_PI*2.0)/M_PI*0.5 - 0.5; CGFloat v = mod(pow(d0*4.0, 0.75),1.0) - 0.5; CGFloat dd = sqrt(u*u+v*v); CGFloat aa = atan(u, v); CGFloat uu = mod(aa*3.0+3.0*cos(dd*30.0-time), M_PI*2.0)/M_PI*0.5 - 0.5; // CGFloat vv = mod(dd*4.0,1.0) - 0.5; CGFloat d2 = sqrt(uu*uu+v*v)*1.5; gl_FragColor = vec4( hsvtorgb(dd+time*0.5/d1, sin(dd*time), d2), 1.0 ); } </script> <script id="fragment-shader-3" type="x-shader/x-fragment"> uniform vec2 resolution; uniform float time; vec2 rand(vec2 pos) { return fract( 0.00005 * (pow(pos+2.0, pos.yx + 1.0) * 22222.0)); } vec2 rand2(vec2 pos) { return rand(rand(pos)); } float softnoise(vec2 pos, float scale) { vec2 smplpos = pos * scale; float c0 = rand2((floor(smplpos) + vec2(0.0, 0.0)) / scale).x; float c1 = rand2((floor(smplpos) + vec2(1.0, 0.0)) / scale).x; float c2 = rand2((floor(smplpos) + vec2(0.0, 1.0)) / scale).x; float c3 = rand2((floor(smplpos) + vec2(1.0, 1.0)) / scale).x; vec2 a = fract(smplpos); return mix( mix(c0, c1, smoothstep(0.0, 1.0, a.x)), mix(c2, c3, smoothstep(0.0, 1.0, a.x)), smoothstep(0.0, 1.0, a.y)); } void main(void) { vec2 pos = gl_FragCoord.xy / resolution.y; pos.x += time * 0.1; float color = 0.0; float s = 1.0; for(int i = 0; i < 8; i++) { color += softnoise(pos+vec2(i)*0.02, s * 4.0) / s / 2.0; s *= 2.0; } gl_FragColor = vec4(color); } </script> <script id="fragment-shader-4" type="x-shader/x-fragment"> uniform float time; uniform vec2 resolution; vec2 rand(vec2 pos) { return fract( pow( pos+2.0, pos.yx+2.0 ) * 555555.0 ); } vec2 rand2(vec2 pos) { return rand(rand(pos)); } float softnoise(vec2 pos, float scale) { vec2 smplpos = pos * scale; float c0 = rand2((floor(smplpos) + vec2(0.0, 0.0)) / scale).x; float c1 = rand2((floor(smplpos) + vec2(1.0, 0.0)) / scale).x; float c2 = rand2((floor(smplpos) + vec2(0.0, 1.0)) / scale).x; float c3 = rand2((floor(smplpos) + vec2(1.0, 1.0)) / scale).x; vec2 a = fract(smplpos); return mix(mix(c0, c1, smoothstep(0.0, 1.0, a.x)), mix(c2, c3, smoothstep(0.0, 1.0, a.x)), smoothstep(0.0, 1.0, a.x)); } void main( void ) { vec2 pos = gl_FragCoord.xy / resolution.y - time * 0.4; float color = 0.0; float s = 1.0; for (int i = 0; i < 6; ++i) { color += softnoise(pos + vec2(0.01 * float(i)), s * 4.0) / s / 2.0; s *= 2.0; } gl_FragColor = vec4(color,mix(color,cos(color),sin(color)),color,1); } </script> <script id="fragment-shader-5" type="x-shader/x-fragment"> uniform float time; uniform vec2 resolution; // tie nd die by Snoep Games. void main( void ) { vec3 color = vec3(1.0, 0., 0.); vec2 pos = (( 1.4 * gl_FragCoord.xy - resolution.xy) / resolution.xx)*1.5; float r=sqrt(pos.x*pos.x+pos.y*pos.y)/15.0; float size1=2.0*cos(time/60.0); float size2=2.5*sin(time/12.1); float rot1=13.00; //82.0+16.0*sin(time/4.0); float rot2=-50.00; //82.0+16.0*sin(time/8.0); float t=sin(time); float a = (60.0)*sin(rot1*atan(pos.x-size1*pos.y/r,pos.y+size1*pos.x/r)+time); //a += 200.0*acos(pos.x*2.0+cos(time/2.0))+asin(pos.y*5.0+sin(time/2.0)); a=a*(r/50.0); a=200.0*sin(a*5.0)*(r/30.0); if(a>5.0) a=a/200.0; if(a<0.5) a=a*22.5; gl_FragColor = vec4( cos(a/20.0),a*cos(a/200.0),sin(a/8.0), 1.0 ); } </script> <script id="fragment-shader-6" type="x-shader/x-fragment"> uniform float time; uniform vec2 resolution; void main( void ) { vec2 uPos = ( gl_FragCoord.xy / resolution.xy );//normalize wrt y axis //suPos -= vec2((resolution.x/resolution.y)/2.0, 0.0);//shift origin to center uPos.x -= 1.0; uPos.y -= 0.5; vec3 color = vec3(0.0); float vertColor = 2.0; for( float i = 0.0; i < 15.0; ++i ) { float t = time * (0.9); uPos.y += sin( uPos.x*i + t+i/2.0 ) * 0.1; float fTemp = abs(1.0 / uPos.y / 100.0); vertColor += fTemp; color += vec3( fTemp*(10.0-i)/10.0, fTemp*i/10.0, pow(fTemp,1.5)*1.5 ); } vec4 color_final = vec4(color, 1.0); gl_FragColor = color_final; } </script> <!-- 运行 Three.js 示例的 Javascript 代码 --> <script type="text/javascript"> var scene; var camera; var render; var webglRender; var canvasRender; var controls; var stats; var guiParams; var ground; var cube; var meshMaterial; var ambientLight; $(function() { stats = initStats(); scene = new THREE.Scene(); webglRender = new THREE.WebGLRenderer( {antialias: true, alpha: true} ); // antialias 抗锯齿 webglRender.setSize(window.innerWidth, window.innerHeight); webglRender.setClearColor(0x000000, 1.0); webglRender.shadowMap.enabled = true; // 允许阴影投射 render = webglRender; camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 10000); // 2147483647 camera.position.set(30, 30, 30); var target = new THREE.Vector3(0, 0 , 0); controls = new THREE.OrbitControls(camera, render.domElement); controls.target = target; camera.lookAt(target); $('#webgl-output')[0].appendChild(render.domElement); window.addEventListener('resize', onWindowResize, false); ambientLight = new THREE.AmbientLight(0x0c0c0c); scene.add(ambientLight); // 定义几何体 var cubeGeometry = new THREE.BoxGeometry(20, 20, 20); // 定义材质 meshMaterial = [ createMaterial('#vertex-shader', '#fragment-shader-1') // 右 ,createMaterial('#vertex-shader', '#fragment-shader-2') // 左 ,createMaterial('#vertex-shader', '#fragment-shader-3') // 上 ,createMaterial('#vertex-shader', '#fragment-shader-4') // 下 ,createMaterial('#vertex-shader', '#fragment-shader-5') // 前 ,createMaterial('#vertex-shader', '#fragment-shader-6') // 后 ] // 定义网格 cube = new THREE.Mesh(cubeGeometry, meshMaterial); scene.add(cube); /** 用来保存那些需要修改的变量 */ guiParams = new function() { this.rotationSpeed = 0.02; this.vertexControl = false; } /** 定义 dat.GUI 对象,并绑定 guiParams 的几个属性 */ var gui = new dat.GUI(); gui.add(guiParams, 'vertexControl'); renderScene(); }); /** 渲染场景 */ function renderScene() { stats.update(); rotateMesh(); // 旋转物体 changeVertex(); requestAnimationFrame(renderScene); render.render(scene, camera); } /** 初始化 stats 统计对象 */ function initStats() { stats = new Stats(); stats.setMode(0); // 0 为监测 FPS;1 为监测渲染时间 $('#stats-output').append(stats.domElement); return stats; } /** 当浏览器窗口大小变化时触发 */ function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); render.setSize(window.innerWidth, window.innerHeight); } /** 旋转物体 */ function rotateMesh() { scene.traverse(function(mesh) { if (mesh instanceof THREE.Mesh && mesh != ground) { mesh.rotation.x += guiParams.rotationSpeed; mesh.rotation.y += guiParams.rotationSpeed; mesh.rotation.z += guiParams.rotationSpeed; } }); } /** 变换方块的每一个顶点 */ function changeVertex() { if (!guiParams.vertexControl) return; cube.material.forEach(function (e) { e.uniforms.time.value += 0.01; }); } /** 自定义创建 ShaderMaterial 材质 */ function createMaterial(vertexShader, fragmentShader) { var vertShader = $(vertexShader).text(); var fragShader = $(fragmentShader).text(); var uniforms = { time: {type: 'f', value: 0.2}, scale: {type: 'f', value: 0.2}, alpha: {type: 'f', value: 0.6}, resolution: {type: 'v2', value: new THREE.Vector2()} }; uniforms.resolution.value.x = window.innerWidth; uniforms.resolution.value.y = window.innerHeight; var shaderMaterial = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: vertShader, fragmentShader: fragShader, transparent: true }); return shaderMaterial; } </script> </body> </html>
其中 id="vertex-shader" 的那段是 vertexShader 着色器脚本,只能用类 C 的 GLSL 语言来写。这里不做深入,只对重要的部分稍作说明。为了能够在 JavaScript 中与着色器进行通信,我们会使用所谓的统一值 uniform,譬如我们在例子中使用语句“uniform float time;” 传入外部数据,根据这个数据,我们会改变传入顶点的 x、y、z 坐标的值(通过 position 变量传入),代码片段如下所示:
vec3 posChanged = position; posChanged.x = posChanged.x*(abs(sin(time*1.0))); posChanged.y = posChanged.y*(abs(cos(time*1.0))); posChanged.z = posChanged.z*(abs(sin(time*1.0)));现在向量 posChanged 中包含的就是顶点的新坐标,通过传入的 time 变量计算得到。最后,我们将这个新坐标传回给 three.js 库,代码如下:
gl_Position = projectionMatrix * modelViewMatrix * vec4(position*(abs(sin(time)/2.0)+0.5),1.0);gl_Position 是一个特殊的变量,用来返回最终的位置。
接着要做的就是要构造一个 ShaderMaterial 对象,并把这个 vertexShader 传给 ShaderMaterial 对象。为此,我们创建了一个简单的辅助函数 createMaterial(vertexShader, fragmentShader),其中两个参数所指的是 HTML 页面中脚本的元素 ID。在这个函数里可以看到我们创建了一个 uniforms 变量,它是用来从我们的渲染器中向着色器传递信息的。此处我们是通过在渲染函数中调用 changeVertex() 函数来到达这个目的,在这个 changeVertex() 函数里,渲染每循环一次就把 time 变量的值增加 0.01,这样就可以把信息传递给我们的 vertexShader 着色器,以便用来计算方块每个顶点的新位置。
另外,通过运行本示例,你可以看到方块的每一个面都在不断变化,正是每个面上的 fragmentShader 片段着色器造就了这种变化。关于 fragmentShader 这部分你可以参考相关的 GLSL 着色器语言知识,这已经完全属于另一门专业的技术范畴了,请读者自行挖掘。
未完待续···