渲染到纹理

这一节实现在纹理中渲染3D场景,这是个很有用的方法,可以用来实现鼠标选取物体、倒影等许多3D效果。

如图22

图22

图22

为实现这个效果,我们可以动态地把板条箱与月球的运动场景“画”到一个纹理缓存中,再把这个纹理缓存作为纹理“贴”到笔记本电脑屏幕位置。

function webGLStart()
{
    //...
    initTextureFramebuffer();
    loadLaptop();
}

先读取一个笔记本电脑模型的json文件,这个和之前的方法一样。

这一节的重头戏是帧缓存(FrameBuffer),它是显示画面的一个直接映象,帧缓存的每一存储单元对应一个要绘制的像素,整个帧缓存对应一帧图像。帧缓存可以包含颜色缓存、深度缓存等的组合。WebGL有一个默认的帧缓存,也就是我们之前一直在用的,绘制到整个canvas的帧缓存。

我们也可以自己额外建立帧缓存,用于在显示区域的特定位置另外渲染3D场景。

var rttFramebuffer;
var rttTexture;
function initTextureFramebuffer()
{
    rttFramebuffer = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, rttFramebuffer);
    rttFramebuffer.width = 512;
    rttFramebuffer.height = 512;

先定义全局变量,建立帧缓存,设置将要使用的帧缓存对应的场景大小,这个帧缓存输出的图像将作为纹理,而纹理尺寸需要是 2 的幂,512在这里是个比较合适的尺寸,256太小,1024没有明显改善。

    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);

建立一个纹理对象,这里和之前相似。

    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA,
        rttFramebuffer.width, rttFramebuffer.height,
        0, gl.RGBA, gl.UNSIGNED_BYTE, null);

texImage2D和之前不大一样,之前我们使用的纹理都来自图片,而这次是来自自己定义的帧缓存渲染的结果。这里使用了texImage2D的另一个版本,告诉显卡准备好特定大小的缓存。最后一个参数用来提供一个数组指针,把数组内容复制到显卡开辟的缓存区,这里给一个null,表示没有要复制的数组。

    var renderbuffer = gl.createRenderbuffer();
    gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);
    gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16,
        rttFramebuffer.width, rttFramebuffer.height);

建立一个渲染缓存,用来为帧缓存的绘制提供处理数据的存储空间,申请了给定长宽的16位数值空间。

    gl.framebufferTexture2D(
        gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, rttTexture, 0);
    gl.framebufferRenderbuffer(
        gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer);

上面建立帧缓存的时候已经bind它了,还是老意思,它作为“当前”帧缓存,这里就给当前帧缓存分配渲染目标(把颜色涂到哪)——刚刚建立的纹理;再给当前帧缓存分配处理深度信息的存储空间——刚刚建立的渲染缓存。

    gl.bindTexture(gl.TEXTURE_2D, null);
    gl.bindRenderbuffer(gl.RENDERBUFFER, null);
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}

然后清理一下三个“当前”。

var laptopAngle = 0;

function drawScene()
{
    gl.bindFramebuffer(gl.FRAMEBUFFER, rttFramebuffer);
    drawSceneOnLaptopScreen();
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);

上面说过,有一个默认的帧缓存,我们绘制都直接绘制到canvas上。这里把“当前”帧缓存设置为rttFramebuffer,drawSceneOnLaptopScreen()是个和之前章节drawScene()一样的绘制函数,把板条箱和月球绘制到“当前”帧缓存“,只不过为了方便,代码中把之前提供表单由用户输入的数据设置为固定值了。

绘制完后,板条箱和月球的场景已经在前面与rttFramebuffer绑定的纹理缓存中了。再把“当前”帧缓存设置为null,那么接下来的绘制又会在默认的帧缓存——我们的canvas中。

    //...

    gl.uniform1i(shaderProgram.showSpecularHighlightsUniform, true);
    gl.uniform3f(shaderProgram.pointLightingLocationUniform, -1, 2, -1);
    gl.uniform3f(shaderProgram.ambientLightingColorUniform, -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);

    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);

    if (laptopVertexPositionBuffer)
    {
        //绘制笔记本电脑模型
    }

在绘制笔记本模型之前对光线进行了一系列设置,这次考虑了不同材质对光线的反射性质有不同,所以不但设置了环境光、漫反射光、高光,还增加设置了材质本身对这三种光反射类型的特定增益/削弱,在片元shader中材质特征会与光反射特征结合计算光照效果。

    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);

    gl.bindBuffer(gl.ARRAY_BUFFER, laptopScreenVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute,
        laptopScreenVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, laptopScreenVertexNormalBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute,
        laptopScreenVertexNormalBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, laptopScreenVertexTextureCoordBuffer);
    gl.vertexAttribPointer(shaderProgram.textureCoordAttribute,
        laptopScreenVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, rttTexture);
    gl.uniform1i(shaderProgram.samplerUniform, 0);

    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLE_STRIP, 0,
        laptopScreenVertexPositionBuffer.numItems);

    mvPopMatrix();
}

画笔记本电脑模型的屏幕,为了真实感,显示屏更多的光应该是自己发出的,所以引入了一个放射光(Emissive Color)的概念,其实它就相当于环境光,只是不跟环境光混淆,这样如果环境光改变(比如变成红色),也不影响模型中屏幕应该“发出”的颜色。

模型的屏幕就是一个贴了纹理的矩形,纹理是前面帧缓存绘制的板条箱和月球,矩形在initBuffer()中设置了顶点、法向量和纹理。

<script id = "per-fragment-lighting-fs" type = "x-shader/x-fragment">
    //...
        if (uUseTextures)
        {
            vec4 textureColor =
                texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
            materialAmbientColor = materialAmbientColor * textureColor.rgb;
            materialDiffuseColor = materialDiffuseColor * textureColor.rgb;
            materialEmissiveColor = materialEmissiveColor * textureColor.rgb;
            alpha = textureColor.a;
        }
        gl_FragColor = vec4(
                           materialAmbientColor * ambientLightWeighting +
                           materialDiffuseColor * diffuseLightWeighting +
                           materialSpecularColor * specularLightWeighting +
                           materialEmissiveColor,
                           alpha
                       );
    }
</script>

结合材质与光照的片元shader,与之前的原理相同,只是增加了一些计算,注意最后加上的materialEmissiveColor。具体参考完整代码。

results matching ""

    No results matching ""