Home >Web Front-end >JS Tutorial >WebGL uses FBO to complete the complete example of cube map effect (with demo source code download)_javascript skills
本文實例講述了WebGL利用FBO完成立方體貼圖效果的方法。分享給大家參考,具體如下:
這篇主要記錄WebGL的一些基本要點,順便也學習下如何使用FBO與環境貼圖。先看下效果圖(需要支援WebGL,Chrome,火狐,IE11)。
主要實現過程如下,先用FBO輸出當前環境在立方體紋理中,再畫出當前立方體,最後畫球,並且把FBO關聯的紋理貼在這個球面上。
開始WebGL時,最好有些OpenGL基礎,在前面講Obj完善與MD2時,大家可能已經發現了,因為著色器的添加使用,原來一些Opengl大部分API已經沒有使用。 WebGL就和這差不多,大部分功能是著色器完成主要功能,記錄下主要過程,大家可以比較下前面的,看看是不是很像,為了熟悉WebGL基本功能,本文沒有利用比較完善的框架,只是用到一個幫助計算矩陣的框架(gl-matrix.js).
和使用OpenGL一樣,我們要初始化使用環境,提取一些全域使用量。相關代碼如下:
初始化:
var gl;//WebGLRenderingContext var cubeVBO;//Cube buffer ID var sphereVBO;//球体VBO var sphereEBO;//球体EBO var cubeTexID;//立方体纹理ID var fboBuffer;//桢缓存对象 var glCubeProgram;//立方体着色器应用 var glSphereProgram;//球体着色器应用 var fboWidth = 512;//桢缓存绑定纹理宽度 var fboHeight = 512;//桢缓存绑定纹理高度 var targets;//立方体贴图六个方向 var pMatrix = mat4.create();//透视矩阵 var vMatrix = mat4.create();//视图矩阵 var eyePos = vec3.fromValues(0.0, 1.0, 0.0);//眼睛位置 var eyeLookat = vec3.fromValues(0.0, -0.0, 0.0);//眼睛方向 var spherePos = vec3.fromValues(0.0, -0.0, 0.0);//球体位置 var canvanName; function webGLStart(cName) { canvanName = cName; InitWebGL(); InitCubeShader(); InitSphereShader(); InitCubeBuffer(); InitSphereBuffer(); InitFBOCube(); //RenderFBO(); gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.enable(gl.DEPTH_TEST); tick(); } function InitWebGL() { //var canvas = document.getElementById(canvanName); InitGL(canvanName); } function InitGL(canvas) { try { //WebGLRenderingContext gl = canvas.getContext("experimental-webgl"); gl.viewportWidth = canvas.width; gl.viewportHeight = canvas.height; targets = [gl.TEXTURE_CUBE_MAP_POSITIVE_X, gl.TEXTURE_CUBE_MAP_NEGATIVE_X, gl.TEXTURE_CUBE_MAP_POSITIVE_Y, gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, gl.TEXTURE_CUBE_MAP_POSITIVE_Z, gl.TEXTURE_CUBE_MAP_NEGATIVE_Z]; } catch (e) { } if (!gl) { alert("你的浏览器不支持WebGL"); } }
在這裡,我們初始化在網頁中WebGL的上下方環境,並給出一系列初始化過程。下面先給出房間,也就是其中立方體的相關代碼。
立方體:
function InitCubeShader() { //WebGLShader var shader_vertex = GetShader("cubeshader-vs"); var shader_fragment = GetShader("cubeshader-fs"); //WebglCubeProgram glCubeProgram = gl.createProgram(); gl.attachShader(glCubeProgram, shader_vertex); gl.attachShader(glCubeProgram, shader_fragment); gl.linkProgram(glCubeProgram); if (!gl.getProgramParameter(glCubeProgram, gl.LINK_STATUS)) { alert("Shader hava error."); } gl.useProgram(glCubeProgram); glCubeProgram.positionAttribute = gl.getAttribLocation(glCubeProgram, "a_position"); glCubeProgram.normalAttribute = gl.getAttribLocation(glCubeProgram, "a_normal"); glCubeProgram.texCoordAttribute = gl.getAttribLocation(glCubeProgram, "a_texCoord"); glCubeProgram.view = gl.getUniformLocation(glCubeProgram, "view"); glCubeProgram.perspective = gl.getUniformLocation(glCubeProgram, "perspective"); } function InitCubeBuffer() { var cubeData = [ -10.0, -10.0, -10.0, 0.0, 0.0, -10.0, 1.0, 0.0, -10.0, 10.0, -10.0, 0.0, 0.0, -10.0, 1.0, 1.0, 10.0, 10.0, -10.0, 0.0, 0.0, -10.0, 0.0, 1.0, 10.0, 10.0, -10.0, 0.0, 0.0, -10.0, 0.0, 1.0, 10.0, -10.0, -10.0, 0.0, 0.0, -10.0, 0.0, 0.0, -10.0, -10.0, -10.0, 0.0, 0.0, -10.0, 1.0, 0.0, -10.0, -10.0, 10.0, 0.0, 0.0, 10.0, 0.0, 0.0, 10.0, -10.0, 10.0, 0.0, 0.0, 10.0, 1.0, 0.0, 10.0, 10.0, 10.0, 0.0, 0.0, 10.0, 1.0, 1.0, 10.0, 10.0, 10.0, 0.0, 0.0, 10.0, 1.0, 1.0, -10.0, 10.0, 10.0, 0.0, 0.0, 10.0, 0.0, 1.0, -10.0, -10.0, 10.0, 0.0, 0.0, 10.0, 0.0, 0.0, -10.0, -10.0, -10.0, 0.0, -10.0, 0.0, 0.0, 0.0, 10.0, -10.0, -10.0, 0.0, -10.0, 0.0, 1.0, 0.0, 10.0, -10.0, 10.0, 0.0, -10.0, 0.0, 1.0, 1.0, 10.0, -10.0, 10.0, 0.0, -10.0, 0.0, 1.0, 1.0, -10.0, -10.0, 10.0, 0.0, -10.0, 0.0, 0.0, 1.0, -10.0, -10.0, -10.0, 0.0, -10.0, 0.0, 0.0, 0.0, 10.0, -10.0, -10.0, 10.0, 0.0, 0.0, 0.0, 0.0, 10.0, 10.0, -10.0, 10.0, 0.0, 0.0, 1.0, 0.0, 10.0, 10.0, 10.0, 10.0, 0.0, 0.0, 1.0, 1.0, 10.0, 10.0, 10.0, 10.0, 0.0, 0.0, 1.0, 1.0, 10.0, -10.0, 10.0, 10.0, 0.0, 0.0, 0.0, 1.0, 10.0, -10.0, -10.0, 10.0, 0.0, 0.0, 0.0, 0.0, 10.0, 10.0, -10.0, 0.0, 10.0, 0.0, 0.0, 0.0, -10.0, 10.0, -10.0, 0.0, 10.0, 0.0, 1.0, 0.0, -10.0, 10.0, 10.0, 0.0, 10.0, 0.0, 1.0, 1.0, -10.0, 10.0, 10.0, 0.0, 10.0, 0.0, 1.0, 1.0, 10.0, 10.0, 10.0, 0.0, 10.0, 0.0, 0.0, 1.0, 10.0, 10.0, -10.0, 0.0, 10.0, 0.0, 0.0, 0.0, -10.0, 10.0, -10.0, -10.0, 0.0, 0.0, 0.0, 0.0, -10.0, -10.0, -10.0, -10.0, 0.0, 0.0, 1.0, 0.0, -10.0, -10.0, 10.0, -10.0, 0.0, 0.0, 1.0, 1.0, -10.0, -10.0, 10.0, -10.0, 0.0, 0.0, 1.0, 1.0, -10.0, 10.0, 10.0, -10.0, 0.0, 0.0, 0.0, 1.0, -10.0, 10.0, -10.0, -10.0, 0.0, 0.0, 0.0, 0.0, ]; cubeVBO = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, cubeVBO); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(cubeData), gl.STATIC_DRAW); } function RenderCube() { gl.useProgram(glCubeProgram); gl.bindBuffer(gl.ARRAY_BUFFER, cubeVBO); gl.vertexAttribPointer(glCubeProgram.positionAttribute, 3, gl.FLOAT, false, 32, 0); gl.enableVertexAttribArray(glCubeProgram.positionAttribute); gl.vertexAttribPointer(glCubeProgram.normalAttribute, 3, gl.FLOAT, false, 32, 12); gl.enableVertexAttribArray(glCubeProgram.normalAttribute); gl.vertexAttribPointer(glCubeProgram.texCoordAttribute, 2, gl.FLOAT, false, 32, 24); gl.enableVertexAttribArray(glCubeProgram.texCoordAttribute); gl.uniformMatrix4fv(glCubeProgram.view, false, vMatrix); gl.uniformMatrix4fv(glCubeProgram.perspective, false, pMatrix); gl.drawArrays(gl.TRIANGLES, 0, 36); }
上面的程式碼主要分為初始化立方體的著色器對象,初始化相關緩存,然後繪製立方體,可以說在Opengl中,如果用著色器來畫,過程也是差不多的,在Opengl裡,已經沒有固定管線的一些功能如InterleavedArrays來指定是頂點還是法線或是紋理了,統一用vertexAttribPointer來傳遞應用程式與著色器之間的資料。在前面 MD2楨動畫實作裡面後面的參數傳遞改進版也有相關應用。
對應著立方體著色器主要代碼如下:
立方體著色器實作:
<script id="cubeshader-fs" type="x-shader/x-fragment"> precision mediump float; varying vec3 normal; varying vec3 tex1; varying vec3 tex2; void main( void ) { float x = tex1.x * 6.28 * 8.0; //2兀 * 8 float y = tex1.y * 6.28 * 8.0; //2兀 * 8 //cos(x)= 8个 (1 -1 1) gl_FragColor = vec4(tex2,1.0) * vec4(sign(cos(x)+cos(y))); // //gl_FragColor = vec4(normal*vec3(0.5)+vec3(0.5), 1); } </script> <script id="cubeshader-vs" type="x-shader/x-vertex"> attribute vec3 a_position; attribute vec3 a_normal; attribute vec2 a_texCoord; uniform mat4 view; uniform mat4 perspective; varying vec3 normal; varying vec3 tex1; varying vec3 tex2; void main( void ) { gl_Position = perspective * view * vec4(a_position,1.0); normal = a_normal; tex1 = vec3(a_texCoord,0.0); tex2 = normalize(a_position)*0.5+0.5; } </script>
著色器中,已經沒有ftransform()功能可供調用,要自己傳遞如模型,視圖,透視矩陣,在這裡,模型是以原點為中心來繪畫,意思模型視圖矩陣也就是視圖矩陣,所以螢幕位置的計算只需要視圖和透視矩陣。在片斷著色器中,x,y是從頂點著色器中的紋理座標傳遞過來,對應過程6.28*8.0,相當於8個360度,用於控制立方體上的方塊顯示,而tex2是著色器中的頂點映射[0,1]的值,分別給立方體的六面分別設定不同的意思,然後用二個向量的乘積來混合這二種顏色顯示,gl_FragColor = vec4(tex2,1.0) * vec4(sign( cos(x)+cos(y)))。
在顯示球體之前,應該先生成當前環境的立方體繪圖,在這裡使用FBO,先生成楨緩存和立方體繪理,並關聯,然後以原點為中心,分別向上下左右前右繪圖,然後利用楨緩衝分別輸出到立方體上的六個面,主要程式碼如下:
FBO與立方體紋理:
function InitFBOCube() { // WebGLFramebuffer fboBuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fboBuffer); fboBuffer.width = 512; fboBuffer.height = 512; cubeTexID = gl.createTexture(); gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubeTexID); gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR); for (var i = 0; i < targets.length; i++) { gl.texImage2D(targets[i], 0, gl.RGBA, fboBuffer.width, fboBuffer.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); } gl.bindFramebuffer(gl.FRAMEBUFFER, null); } function RenderFBO() { gl.disable(gl.DEPTH_TEST); gl.viewport(0, 0, fboBuffer.width, fboBuffer.height); gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.bindFramebuffer(gl.FRAMEBUFFER, fboBuffer); for (var i = 0; i < targets.length; i++) { gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, targets[i], cubeTexID, null); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); } mat4.perspective(pMatrix, 45, fboBuffer.width / fboBuffer.height, 0.1, 100.0); for (var i = 0; i < targets.length; i++) { gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, targets[i], cubeTexID, null); var lookat = vec3.create(); var up = vec3.create(); up[1] = 1.0; if (i == 0) { lookat[0] = -1.0; } else if (i == 1) { lookat[0] = 1.0; } else if (i == 2) { lookat[1] = -1.0; up[0] = 1.0; } else if (i == 3) { lookat[1] = 1.0; up[0] = 1.0; } else if (i == 4) { lookat[2] == -1.0; } else if (i == 5) { lookat[2] = 1.0; } else { } //vec3.fromValues(0.0, 0.0, 0.0) vMatrix = mat4.create(); mat4.lookAt(vMatrix, vec3.fromValues(0.0, 0.0, 0.0), lookat, up); //mat4.scale(vMatrix, vMatrix, vec3.fromValues(-1.0, -1.0, -1.0)); //mat4.translate(vMatrix, vMatrix, spherePos); RenderCube(); } gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.enable(gl.DEPTH_TEST); }
在上面不知是gl-matrix提供的矩陣演算法有問題,還是本來應該這樣,在上下面的時候產生的紋理圖不對,需要偏轉攝影機的向上向量。因為這是攝影機位置與目標的產生的Z軸和設定的UP軸平行了,這樣導致不能正確計算X軸,然後對應的UP軸也計算不出來,相應視圖矩陣出現錯誤。
最後是球體的繪畫,程式碼主要和立方體的差不多,注意球體的頂點演算法。
球體:
function InitSphereShader() { //WebGLShader var shader_vertex = GetShader("sphereshader-vs"); var shader_fragment = GetShader("sphereshader-fs"); //WebglCubeProgram glSphereProgram = gl.createProgram(); gl.attachShader(glSphereProgram, shader_vertex); gl.attachShader(glSphereProgram, shader_fragment); gl.linkProgram(glSphereProgram); if (!gl.getProgramParameter(glSphereProgram, gl.LINK_STATUS)) { alert("Shader hava error."); } glSphereProgram.positionAttribute = gl.getAttribLocation(glSphereProgram, "a_position"); glSphereProgram.normalAttribute = gl.getAttribLocation(glSphereProgram, "a_normal"); glSphereProgram.eye = gl.getUniformLocation(glSphereProgram, "eye"); glSphereProgram.mapCube = gl.getUniformLocation(glSphereProgram, "mapCube"); glSphereProgram.model = gl.getUniformLocation(glSphereProgram, "model"); glSphereProgram.view = gl.getUniformLocation(glSphereProgram, "view"); glSphereProgram.perspective = gl.getUniformLocation(glSphereProgram, "perspective"); } function InitSphereBuffer() { var radius = 1; var segments = 16; var rings = 16; var length = segments * rings * 6; var sphereData = new Array(); var sphereIndex = new Array(); for (var y = 0; y < rings; y++) { var phi = (y / (rings - 1)) * Math.PI; for (var x = 0; x < segments; x++) { var theta = (x / (segments - 1)) * 2 * Math.PI; sphereData.push(radius * Math.sin(phi) * Math.cos(theta)); sphereData.push(radius * Math.cos(phi)); sphereData.push(radius * Math.sin(phi) * Math.sin(theta)); sphereData.push(Math.sin(phi) * Math.cos(theta)); sphereData.push(radius * Math.cos(phi)) sphereData.push(Math.sin(phi) * Math.sin(theta)); } } for (var y = 0; y < rings - 1; y++) { for (var x = 0; x < segments - 1; x++) { sphereIndex.push((y + 0) * segments + x); sphereIndex.push((y + 1) * segments + x); sphereIndex.push((y + 1) * segments + x + 1); sphereIndex.push((y + 1) * segments + x + 1); sphereIndex.push((y + 0) * segments + x + 1) sphereIndex.push((y + 0) * segments + x); } } sphereVBO = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, sphereVBO); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(sphereData), gl.STATIC_DRAW); sphereVBO.numItems = segments * rings; sphereEBO = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, sphereEBO); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(sphereIndex), gl.STATIC_DRAW); sphereEBO.numItems = sphereIndex.length; } function RenderSphere() { gl.useProgram(glSphereProgram); gl.bindBuffer(gl.ARRAY_BUFFER, sphereVBO); gl.vertexAttribPointer(glSphereProgram.positionAttribute, 3, gl.FLOAT, false, 24, 0); gl.enableVertexAttribArray(glSphereProgram.positionAttribute); gl.vertexAttribPointer(glSphereProgram.normalAttribute, 3, gl.FLOAT, false, 24, 12); gl.enableVertexAttribArray(glSphereProgram.normalAttribute); var mMatrix = mat4.create(); mat4.translate(mMatrix, mMatrix, spherePos); gl.uniform3f(glSphereProgram.eye, eyePos[0],eyePos[1],eyePos[2]); gl.uniformMatrix4fv(glSphereProgram.model, false, mMatrix); gl.uniformMatrix4fv(glSphereProgram.view, false, vMatrix); gl.uniformMatrix4fv(glSphereProgram.perspective, false, pMatrix); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubeTexID); //gl.uniformMatrix4fv(glSphereProgram.mapCube, 0); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, sphereEBO); gl.drawElements(gl.TRIANGLES, sphereEBO.numItems, gl.UNSIGNED_SHORT, 0); gl.bindTexture(gl.TEXTURE_2D, null); }
可以看到,也是和立方體一樣的三步,初始化著色器,初始化頂點與法線,繪畫。下面給出著色器程式碼:
球體著色器:
<script id="sphereshader-fs" type="x-shader/x-fragment"> precision mediump float; varying vec3 normal; varying vec3 eyevec; uniform samplerCube mapCube; void main( void ) { gl_FragColor = textureCube(mapCube, reflect(normalize(-eyevec), normalize(normal))); } </script> <script id="sphereshader-vs" type="x-shader/x-vertex"> attribute vec3 a_position; attribute vec3 a_normal; uniform mat4 model; uniform mat4 view; uniform mat4 perspective; uniform vec3 eye; varying vec3 normal; varying vec3 eyevec; void main( void ) { gl_Position = perspective * view * model * vec4(a_position,1.0); eyevec = -eye;// a_position.xyz; normal = a_normal; } </script>
和前面立方體有點不同的是,球體有自己的模型矩陣,這也是一般正常的用法,然後傳遞眼睛對應球體頂點向量與法線傳遞在片斷著色器中,在片斷著色器中,就有用到前面所產生的立方體紋理,我們根據眼睛經過頂點透過對應法向量反射到立體體紋理上的點來取得目前球體所對應的環境顏色,在這裡,我們可以直接呼叫textureCube來完成上面所說的過程,不需要我們手動來計算。
其中GetShader函數的使用,參考了http://msdn.microsoft.com/zh-TW/library/ie/dn302360(v=vs.85) 這裡的解說。
可以說,上面主要的繪製函數已經完成,但是我們這個是能動的,所以需要模擬如客戶端環境每隔多久繪製一次,主要程式碼如下:
動畫:
function tick() { Update(); OnDraw(); setTimeout(function () { tick() }, 15); } function OnDraw() { //fbo rander CUBE_MAP RenderFBO(); //element rander gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); mat4.perspective(pMatrix, 45, gl.viewportWidth / gl.viewportHeight, 0.1, 200.0); mat4.lookAt(vMatrix, eyePos, eyeLookat, vec3.fromValues(0.0, 1.0, 0.0)); RenderCube(); RenderSphere(); } var lastTime = new Date().getTime(); function Update() { var timeNow = new Date().getTime(); if (lastTime != 0) { var elapsed = timeNow - lastTime; //3000控制人眼的旋转速度。8控制人眼的远近 eyePos[0] = Math.cos(elapsed / 3000) * 8; eyePos[2] = Math.sin(elapsed / 2000) * 8; spherePos[0] = Math.cos(elapsed / 4000) * 3; spherePos[2] = Math.cos(elapsed / 4000) * 3; } }
In the above, the Update and Draw functions are called every 15 milliseconds, where Update is used to update the eye and sphere positions, and Draw is used for painting.
Click here for complete example codeDownload from this site.
Readers who are interested in more content related to JS special effects can check out the special topics on this site: "Summary of jQuery animation and special effects usage" and "Summary of common classic special effects of jQuery"
I hope this article will be helpful to everyone in JavaScript programming.