2022-10-12-------------------webgl实现简单的法线贴图

126 阅读1分钟

一、思路

  1. 将法线x,y,z三个分量存储在法线贴图存储在r,g,b三个通道中
  2. 利用模型的切线T和法向量N(模型的真正法线)计算副切线构建TBN矩阵
  3. 从法线贴图中rgb值转化到-1到1,即为切线空间normal
  4. TBN矩阵乘normal转化为世界空间切线
  5. 后续步骤通过光照模型计算

二、代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Title</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
        }

        #canvas {
            margin: 0;
            display: block;
        }

        div {
            position: absolute;
            left: 100px;
            top: 100px;
            z-index: 999;
        }
    </style>
</head>

<body onload="main()">
    <canvas id="canvas" width="1024" height="1024"></canvas>
</body>
<script src="/lib/webgl-utils.js"></script>
<script src="/lib/webgl-debug.js"></script>
<script src="/lib/cuon-utils.js"></script>
<script src="/lib/cuon-matrix.js"></script>
<script>
    //设置WebGL全屏显示
    var canvas = document.getElementById("canvas");
    //设置顶点着色器
    var vertexShaderSource = /*glsl*/ `
            attribute vec4 a_Position;
            attribute vec2 a_TexCoord;
            attribute vec3 a_Color;
            uniform mat4 u_MvpMatrix;
            varying vec2 v_TexCoord;
            varying vec3 v_Color;
            varying mat3 v_TBN;

            void main(){
               gl_Position = u_MvpMatrix * a_Position;
               v_TexCoord = a_TexCoord;
               v_Color = a_Color;
                 vec3 normal=normalize(vec3(0.0,0.0,1.0));
                vec3 tanVec=vec3(1.0,0.0,0.0);
                vec3 bitTan=normalize(cross(tanVec,normal));
                v_TBN=mat3(tanVec,bitTan,normal);
            }`;

    //设置片元着色器
    var fragmentShaderSource = /*glsl*/ `
            #ifdef GL_ES
            precision mediump float;
            #endif
            uniform sampler2D u_Sampler;
            uniform float u_Number;
            varying vec2 v_TexCoord;
            varying vec3 v_Color;
             varying mat3 v_TBN;
            void main(){
                vec3 lightdir=vec3(1.0,-1.0,0.0);
                vec3 lightColor=vec3(5.0/255.0,140.0/255.0,255.0/255.0);
                vec4 textureColor=texture2D(u_Sampler,vec2(fract(u_Number)+v_TexCoord.x,fract(u_Number)+v_TexCoord.y));
                //转化为-1->1
                vec3 normal=normalize(textureColor.rgb*2.0-1.0);
                vec3 worldNormal=normalize(v_TBN*normal);

                //计算漫反射
                float dotL=max(0.0,dot(normalize(-lightdir),worldNormal));
                vec3 diffcuseColor=lightColor*dotL;
               
               gl_FragColor =vec4(diffcuseColor+vec3(0.0,0.1,0.2),0.9);
            }`;

    //设置FBO的宽和高
    var offset_width = 1024;
    var offset_height = 1024;

    //平面绕z轴的角度
    var z_planeAngle = 0;

    //平面的模型矩阵,设置初始化为向上移动一个单位
    var plane_modelMatrix = new Matrix4();
    plane_modelMatrix.setTranslate(0, 1, 0);

    //设置视图矩阵
    const viewMatrix = new Matrix4();
    viewMatrix.setLookAt(0.0, 3.5, 5.0, 0.0, 0, 0.0, 0.0, 1, 0);

    function main() {
        //----------------------使用键盘控制立方体的移动-----------------------------------
        const moveSpeed = 0.1;
        document.addEventListener("keydown", (e) => {
            if (e.key === "ArrowUp") {
                cubeModelMatrix.translate(0, moveSpeed, 0);
            } else if (e.key === "ArrowDown") {
                cubeModelMatrix.translate(0, -moveSpeed, 0);
            } else if (e.key === "ArrowLeft") {
                cubeModelMatrix.translate(-moveSpeed, 0, 0);
            } else if (e.key === "ArrowRight") {
                cubeModelMatrix.translate(moveSpeed, 0, 0);
            }
        });
        var gl = getWebGLContext(canvas);

        if (!gl) {
            console.log("无法获取WebGL的上下文");
            return;
        }

        //-----------------创建着色器----------------------
        const program = createProgram(
            gl,
            vertexShaderSource,
            fragmentShaderSource
        );
        if (!program) {
            console.error("创建程序失败!");
        }

        //设置立方体着色器中相关的变量
        program.a_Position = gl.getAttribLocation(program, "a_Position");
        program.a_TexCoord = gl.getAttribLocation(program, "a_TexCoord");
        program.a_Color = gl.getAttribLocation(program, "a_Color");
        program.u_MvpMatrix = gl.getUniformLocation(program, "u_MvpMatrix");
        program.u_Sampler = gl.getUniformLocation(program, "u_Sampler");
        program.u_Number = gl.getUniformLocation(program, "u_Number");
        if (
            program.a_Position < 0 ||
            program.a_TexCoord < 0 ||
            !program.u_MvpMatrix ||
            !program.a_Color ||
            !program.u_Sampler ||
            !program.u_Number
        ) {
            console.log("无法获取到变量的存储位置");
            return;
        }

        //设置顶点的位置信息

        var plane = initVertexBuffersForPlane(gl);

        if (!plane) {
            console.log("存入缓冲区数据失败");
            return;
        }

        //给立方体创建纹理
        var texture = createTexture(gl);
        if (!texture) {
            console.log("无法创建纹理缓冲区");
            return;
        }

        //开启隐藏面消除功能
        gl.enable(gl.DEPTH_TEST);
        //开启背面剔除
        // gl.enable(gl.CULL_FACE);

        //设置视图投影矩阵相关信息
        var viewProjectMatrix = new Matrix4();
        viewProjectMatrix.setPerspective(
            80.0,
            canvas.width / canvas.height,
            0.1,
            1000.0
        );

        viewProjectMatrix.multiply(viewMatrix);

        //开始绘制
        var currentAngle = 0.0;
        var number = 0;
        /**
         * 循环绘制
         * */
        function tick() {
            currentAngle = animate(currentAngle); //更新旋转角度
            number += 0.001;
            draw(
                gl,
                plane,
                currentAngle,
                texture,
                viewProjectMatrix,
                program,
                number
            );
            requestAnimationFrame(tick);
        }

        tick();
    }

    const rotateMatrix = new Matrix4();

    /**
     * 绘制
     * */
    function draw(
        gl,
        plane,
        angle,
        texture,
        viewProjectMatrix,
        program,
        number
    ) {
        gl.viewport(0, 0, canvas.width, canvas.height); //将视口的大小设置为canvas的大小

        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); //清除背景和隐藏面消除

        // 绘制平面
        gl.useProgram(program);
        gl.uniform1f(program.u_Number, number);
        drawTexturedPlane(gl, program, plane, 0.0, texture, viewProjectMatrix);
    }

    //声明mvp矩阵
    var g_mvpMatrix = new Matrix4();

    /**
     * 绘制平面
     * */
    function drawTexturedPlane(
        gl,
        program,
        obj,
        angle,
        texture,
        viewProjectMatrix
    ) {
        const modelMatrix = new Matrix4();
        modelMatrix.set(plane_modelMatrix);
        modelMatrix.scale(4, 1, 4);

        g_mvpMatrix.set(viewProjectMatrix);
        g_mvpMatrix.multiply(modelMatrix);
        gl.uniformMatrix4fv(program.u_MvpMatrix, false, g_mvpMatrix.elements);
        gl.uniform1i(program.u_Sampler, 0);

        //   gl.uniform1f(program.u_Number, 0.8);
        drawTexturedObject(gl, program, obj, texture);
    }

    function drawTexturedObject(gl, program, obj, texture) {
        //分配缓冲区对象并启用赋值
        initAttributeVariable(gl, program.a_Position, obj.vertexBuffer); //顶点坐标

        initAttributeVariable(gl, program.a_Color, obj.colorBuffer); //颜色

        initAttributeVariable(gl, program.a_TexCoord, obj.texCoordBuffer); //纹理坐标

        //将纹理对象绑定到目标
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, texture);

        //绘制
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, obj.indexBuffer);
        gl.drawElements(gl.TRIANGLES, obj.numIndices, obj.indexBuffer.type, 0);
    }

    /**
     * 给变量写定缓冲区
     * */
    function initAttributeVariable(gl, a_attribute, buffer) {
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

        gl.vertexAttribPointer(a_attribute, buffer.num, buffer.type, false, 0, 0);
        gl.enableVertexAttribArray(a_attribute);
    }

    var angle_step = 30;
    var last = +new Date();

    function animate(angle) {
        var now = +new Date();
        var elapsed = now - last;
        last = now;
        var newAngle = angle + (angle_step * elapsed) / 1000.0;
        return newAngle % 360;
    }

    /**
     *  创建纹理
     */
    function createTexture(gl) {
        var img = new Image();
        img.src = "/image/waterNormals.jpg";
        var texture = gl.createTexture();
        if (!texture) {
            console.log("无法创建纹理缓冲区");
            return null;
        }
        img.onload = function () {
            gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
            gl.bindTexture(gl.TEXTURE_2D, texture);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
            gl.texImage2D(
                gl.TEXTURE_2D,
                0,
                gl.RGBA,
                gl.RGBA,
                gl.UNSIGNED_BYTE,
                img
            );
            gl.bindTexture(gl.TEXTURE_2D, null);
        };

        return texture;
    }

    function initVertexBuffersForPlane(gl) {
        // 创建一个面
        //  v1------v0
        //  |        |
        //  |        |
        //  |        |
        //  v2------v3

        // 顶点的坐标
        var vertices = new Float32Array([
            1.0,
            0.0,
            1.0,
            -1.0,
            0.0,
            1.0,
            -1.0,
            0.0,
            -1.0,
            1.0,
            0.0,
            -1.0, // v0-v1-v2-v3
        ]);
        var colors = new Float32Array([
            0.0,
            0.0,
            1.0,
            0.0,
            0.0,
            1.0,
            0.0,
            0.0,
            1.0,
            0.0,
            0.0,
            1.0, // v0-v1-v2-v3
        ]);
        // 纹理的坐标
        var texCoords = new Float32Array([
            1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0,
        ]);

        // 顶点的索引
        var indices = new Uint8Array([0, 3, 2, 0, 2, 1]);

        //将顶点的信息写入缓冲区对象
        var obj = {};
        obj.vertexBuffer = initArrayBufferForLaterUse(gl, vertices, 3, gl.FLOAT);
        obj.texCoordBuffer = initArrayBufferForLaterUse(
            gl,
            texCoords,
            2,
            gl.FLOAT
        );
        obj.colorBuffer = initArrayBufferForLaterUse(gl, colors, 3, gl.FLOAT);
        obj.indexBuffer = initElementArrayBufferForLaterUse(
            gl,
            indices,
            gl.UNSIGNED_BYTE
        );
        if (!obj.vertexBuffer || !obj.texCoordBuffer || !obj.indexBuffer)
            return null;

        obj.numIndices = indices.length;

        gl.bindBuffer(gl.ARRAY_BUFFER, null);
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

        return obj;
    }

    function initArrayBufferForLaterUse(gl, data, num, type) {
        var buffer = gl.createBuffer();
        if (!buffer) {
            console.log("无法创建缓冲区对象");
            return null;
        }

        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

        buffer.num = num;
        buffer.type = type;

        return buffer;
    }

    function initElementArrayBufferForLaterUse(gl, data, type) {
        var buffer = gl.createBuffer();
        if (!buffer) {
            console.log("无法创建着色器");
            return null;
        }

        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW);

        buffer.type = type;

        return buffer;
    }
</script>

</html>

三、结果

10-08-32.gif