three.js 04-08 之 ShaderMaterial 材質

    今天我們將要介紹的高級材質叫 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 着色器語言知識,這已經完全屬於另一門專業的技術範疇了,請讀者自行挖掘。

未完待續···


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章