WebGL教程17: 渲染到纹理
HiWebGL译者声明:因为译者个人方便的原因,我们将原教程中的第三方图形库由glMatrix改为Oak3D实现,这不影响到Demo的最终效果和实现,也不影响到WebGL的讲解和学习。原教程正文中相应的代码和讲解也为做了相应修改!本教程由HiWebGL翻译整理,转载请注明出处!
关于Oak3D:Oak3D是一套简单易用、性能优越的WebGL Javascript Library。您可以在他们的主页找到更多信息。Oak3D主页:http://www.oak3d.com
欢迎来到系列教程的第16课!在这一课中,我们将介绍一种非常有用的绘制技术: 将3D场景渲染到一张纹理中。通过这一技术,我们可以在绘制过程中利用上一次绘制的结果来创造一些特殊的效果。同时,这也是一种常用的绘制技巧,除了在一个场景中绘制另一个场景(本课将详细解释这种应用途径)外,渲染到纹理也是做选取(鼠标在3D场景中点选物体)、引用、反射等3D特效的基础技术。
下面的视频就是我们这节课将会完成的最终效果。
在本课的演示程序中,你会看到一个综合了多种光照效果(包含笔记本屏幕上的高光)的白色笔记本模型。除了笔记本模型,你会发现一些更加有趣的东西,在笔记本的屏幕上显示了另外一个3D场景,对,没错,那是13课中我们看过的月球和木箱。这个演示程序的思路很清楚,它将13课中的3D场景渲染到了一张2D纹理上,然后将这张2D纹理映射到了笔记本模型的屏幕上。
下面我们来看看它是怎么工作的……
惯例声明:本系列的教程是针对那些已经具备相应编程知识但没有实际3D图形经验的人的;目标是让学习者创建并运行代码,并且明白代码其中的含义,从而可以快速地创建自己的3D Web页面。如果你还没有阅读前一课,请先阅读前一课的内容吧。因为本课中我只会讲解那些与前一课中不同的新知识。
另外,我编写这套教程是因为我在独立学习WebGL,所以教程中可能(非常可能)会有错误,所以还请风险自担。尽管如此,我还是会不断的修正bug和改正其中的错误的,所以如果你发现了教程中的错误,请告诉我。
有两种方法可以获得上面实例的代码:在实例的独立页面中选择“查看源代码”;或者点击这里下载我们为您准备好的压缩包。
你可以使用任何你所熟悉的文本编辑器来查看课程源码(index.html)。本课内容的文件组织与前几课有较大的不同,下面让我们按由下向上的顺序来查看代码内容。首先是webGLStart,发生变化的代码位于第789行。
786 787 788 789 790 791 792 793 794 795 796 797 798 799 |
function webGLStart() { var canvas = document.getElementById("lesson16-canvas"); initGL(canvas); initTextureFramebuffer(); initShaders(); initBuffers(); initTextures(); loadLaptop(); gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.enable(gl.DEPTH_TEST); tick(); } |
当你使用WebGL API渲染3D内容时,显卡需要一块缓冲区来存储渲染的最终结果。通过WebGL 接口,你实际上可以控制这块存储区域的结构类型。首先,你至少需要一块区域来存储每个像素渲染后的颜色结果,同时,你往往还需要一个depth buffer(深度缓冲区)来处理视线遮挡,这样,近处的像素将会遮挡住较远处的像素,depth buffer同样需要一部分存储空间。除此之外,有时我们也会用到一些其它种类的buffer,比如stencil buffer(模板缓冲区),在接下来的系列教程中,我们会讲到它。
Framebuffer就是用来存储渲染结果的这一类缓冲区的一种集合。如果你没有指定,默认会存在一个”Default” frame buffer,也就是到本课之前,我们一直在使用的frame buffer,它代表最终会被显示在网页中的缓冲区域。除此之外,你可以创建你自己的frame buffer,并指定WebGL将渲染结果输送到你所创建的frame buffer中。在本课中,我们创建了一个自己的frame buffer,并指定它使用一张纹理作为存储像素颜色的缓冲区。同时,我们也需要分配一个深度缓冲区来做深度遮挡计算。
下面,让我们来看一下创建frame buffer的具体代码。在本课的实例中,initTextureFramebuffer函数完成了这个工作,如果你需要查找这个函数,它大概在文件中从上向下浏览的三分之一处。
258 259 260 261 |
var rttFramebuffer; var rttTexture; function initTextureFramebuffer() { |
262 263 264 265 |
rttFramebuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, rttFramebuffer); rttFramebuffer.width = 512; rttFramebuffer.height = 512; |
接下来,我们创建一个纹理对象,并初始化相应的参数。
267 268 269 270 271 |
rttTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, rttTexture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST); gl.generateMipmap(gl.TEXTURE_2D); |
273 |
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, rttFramebuffer.width, |
通常,当我们想为texture加载一张图片时,我们会使用gl.texImage2D来为texture指定对应的Image,但在这里,我们并没有图片可用来加载,所以我们调用了gl.texImage2D 的另外一个版本,告诉WebGL我们并没有Image可供加载,我们仅仅需要创建一个指定大小的空数据的纹理对象。严格来说,这里的最后一个参数是用来指定传入纹理的像素列表,但在这里,我们不需要纹理对象加载任何数据,所以在这里,我们传入null。(早期的Minefield 需要使用者传入一个指定大小的空数组来初始化纹理,但这个问题目前似乎已经被修复掉了)
现在我们拥有了一张用来存储颜色结果的纹理,接下来,我们需要创建一张depth buffer用来记录深度信息。
275 276 277 |
var renderbuffer = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, |
这里我们创建了一个render buffer对象,render buffer表示用来存储与frame buffer相关的广义用途的缓冲区,你可以用它来作为深度缓冲,或者模板缓冲,或者两者兼之。像其它缓冲区对象一样,我们绑定render buffer,将它作为WebGL的当前render buffer,并调用gl.renderbufferStorage来通知WebGL当前render buffer用作深度缓冲区,每个像素的深度数据大小16位,同时我们还指定了render buffer的尺寸。
接下来:
279 280 |
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, |
我们将创建的纹理和render buffer都绑定到当前的frame buffer上(不要忘记,在frame buffer被创建后,我们就一直将它作为WebGL的当前frame buffer)。这里,我们告诉WebGL当前frame buffer使用rttTexture作为颜色缓冲区(gl.COLOR_ATTACHMENT0),使用我们创建的render buffer作为深度缓冲区(gl.DEPTH_ATTACHMENT)。
到此为止,frame buffer的内容初始化工作就完成了,WebGL已经知道我们所创建的frame buffer该绘制到什么地方去;所以,在这之后,我们将当前的texture、renderbuffer和framebuffer重置为默认状态。
282 283 284 |
gl.bindTexture(gl.TEXTURE_2D, null); gl.bindRenderbuffer(gl.RENDERBUFFER, null); gl.bindFramebuffer(gl.FRAMEBUFFER, null); |
685 686 687 688 689 690 691 692 693 694 695 696 |
var laptopAngle = 0; function drawScene() { gl.bindFramebuffer(gl.FRAMEBUFFER, rttFramebuffer); drawSceneOnLaptopScreen(); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) pMatrix = okMat4Proj(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0); |
所以,当drawScene函数开头的三行代码执行后,我们就拥有了一张存储着第13课场景绘制结果的纹理。绘制代码的剩余部分就是正常绘制出笔记本模型,并将之前存储着场景绘制结果的纹理映射到笔记本模型的屏幕上。首先是初始化model-view矩阵,并将笔记本旋转一定的角度,角度由laptopAngle给出(像之前的课程一样,laptopAngle每帧都会在Animate函数中更新来实现笔记本模型的持续旋转效果)。
698 699 700 701 702 703 704 |
mvMatrix.identity(); mvPushMatrix(); mvMatrix.translate(OAK.SPACE_LOCAL, 0, -0.4, -2.2, true); mvMatrix.rotY(OAK.SPACE_LOCAL, laptopAngle, true); mvMatrix.rotX(OAK.SPACE_LOCAL, -90, true); |
706 707 708 709 710 711 |
gl.uniform1i(shaderProgram.showSpecularHighlightsUniform, true); gl.uniform3f(shaderProgram.pointLightingLocationUniform, -1, 2, -1); gl.uniform3f(shaderProgram.ambientLightingColorUniform, 0.2, 0.2, 0.2); gl.uniform3f(shaderProgram.pointLightingDiffuseColorUniform, 0.8, 0.8, 0.8); gl.uniform3f(shaderProgram.pointLightingSpecularColorUniform, 0.8, 0.8, 0.8); |
713 714 715 716 717 718 719 |
// The laptop body is quite shiny and has no texture. It reflects lots of specular light gl.uniform3f(shaderProgram.materialAmbientColorUniform, 1.0, 1.0, 1.0); gl.uniform3f(shaderProgram.materialDiffuseColorUniform, 1.0, 1.0, 1.0); gl.uniform3f(shaderProgram.materialSpecularColorUniform, 1.5, 1.5, 1.5); gl.uniform1f(shaderProgram.materialShininessUniform, 5); gl.uniform3f(shaderProgram.materialEmissiveColorUniform, 0.0, 0.0, 0.0); gl.uniform1i(shaderProgram.useTexturesUniform, false); |
721 722 723 724 725 726 727 728 729 730 731 732 733 734 |
if (laptopVertexPositionBuffer) { gl.bindBuffer(gl.ARRAY_BUFFER, laptopVertexPositionBuffer); gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, |
上面的代码已经将笔记本模型绘制在了网页上,下面,我们需要将纹理映射到笔记本的屏幕上。首先依然是需要设置光照参数,在这里,我们设置了不同的emissive colour。
736 737 738 739 740 741 |
gl.uniform3f(shaderProgram.materialAmbientColorUniform, 0.0, 0.0, 0.0); gl.uniform3f(shaderProgram.materialDiffuseColorUniform, 0.0, 0.0, 0.0); gl.uniform3f(shaderProgram.materialSpecularColorUniform, 0.5, 0.5, 0.5); gl.uniform1f(shaderProgram.materialShininessUniform, 20); gl.uniform3f(shaderProgram.materialEmissiveColorUniform, 1.5, 1.5, 1.5); gl.uniform1i(shaderProgram.useTexturesUniform, true); |
752 753 754 |
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, rttTexture); gl.uniform1i(shaderProgram.samplerUniform, 0); |
756 757 758 759 |
setMatrixUniforms(); gl.drawArrays(gl.TRIANGLE_STRIP, 0, laptopScreenVertexPositionBuffer.numItems); mvPopMatrix(); |
在本文的最后,我们给出了一个用于新材质类型光照的fragment shader,在理解之前课程的基础上,新shader的内容应该非常容易理解。这里唯一的新内容就是增加了emissive colour,而shader对它的处理仅仅的将它简单的加在最终的像素颜色上:
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
precision mediump float; varying vec2 vTextureCoord; varying vec3 vTransformedNormal; varying vec4 vPosition; uniform vec3 uMaterialAmbientColor; uniform vec3 uMaterialDiffuseColor; uniform vec3 uMaterialSpecularColor; uniform float uMaterialShininess; uniform vec3 uMaterialEmissiveColor; uniform bool uShowSpecularHighlights; uniform bool uUseTextures; uniform vec3 uAmbientLightingColor; uniform vec3 uPointLightingLocation; uniform vec3 uPointLightingDiffuseColor; uniform vec3 uPointLightingSpecularColor; uniform sampler2D uSampler; void main(void) { vec3 ambientLightWeighting = uAmbientLightingColor; vec3 lightDirection = normalize(uPointLightingLocation - vPosition.xyz); vec3 normal = normalize(vTransformedNormal); vec3 specularLightWeighting = vec3(0.0, 0.0, 0.0); if (uShowSpecularHighlights) { vec3 eyeDirection = normalize(-vPosition.xyz); vec3 reflectionDirection = reflect(-lightDirection, normal); float specularLightBrightness = pow(max(dot(reflectionDirection, eyeDirection), 0.0), |