化石原创文章,转载请注明来源并保留原文链接


WebGL中,使用html本身的元素做UI能借助浏览器本身的渲染能力和布局能力,在大多数场合都非常有效。刚入手WebGL的时候,很多人跟本人一样,可能缺乏对HTML的深度认知,因此不知道怎么能让html元素和WebGL的目标canvas做一定的整合。这里做一定的步骤说明。

1、使用一个div,包起canvas和canvas重合的元素。

2、canvas的style,带上:position:absolute;

3、和canvas重合的元素,假设用一个div都包起来的话,这个div同样带上:position:absolute;

原理说明:

1、position: absolute 这个会让div或者canvas都以父元素的位置绝对定位。

2、因为canvas和跟它重合的元素都是以同一个div做父元素,因此,两者就能够在位置上重合。

3、在这个做法上,canvas需要写在UI元素的前面。也就是让浏览器先渲染canvas,再渲染UI。

一段演示的html代码如下:

<html>

<head>
    <title>Test</title>
</head>

<body>
    <div id="mainLayout" style="width: 1280; margin: auto; ">
        <p>这里是html的元素,不在canvas重合区域的部分</p>
    </div>
    <div id="canvasLayout" style="width: 1280; margin: auto; ">
        <canvas id="canvas" style="position: absolute; width: 500px; height: 500px;"></canvas>
        <div id="canvashud" style="position: absolute;">
            <p>这里是canvas重合的部分的字</p>
            <button>这里是canvas重合的部分的按钮</button>
        </div>
    </div>
    <script>
         var canvas = document.getElementById('canvas');
         var gl = canvas.getContext('experimental-webgl');
         gl.clearColor(0.5, 0.5, 0.5, 0.9);
         gl.clear(gl.COLOR_BUFFER_BIT);
    </script>
</body>

</html>

化石原创文章,转载请注明来源并保留原文链接



化石原创文章,转载请注明来源并保留原文链接


对于Texture的封装,我们暂时没有太多的代码,只需要符合requirejs规范,使用javascript的image即可。下面是相关的代码:

define(['../core/defineProperties',
    './ImageCache',
    ], function(
        defineProperties,
        ImageCache,
    ) 
{

    function createTexture(image, gl) {
        var tex = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, tex);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
        return tex;
    }

    function init(texture, callback) {
        const image = new Image();
        image.onload = function(event) {
            ImageCache[texture._path] = image;
            var gl = texture._gl;
            texture._texture = createTexture(image, gl);
            texture._ready = true;
            if (callback != null) {
                callback(texture._texture);
            }
        }
        image.src = texture._path;
    }



    /**
     * class of texture
     *
     * @param {Object} [options] Object with the following properties:
     * @param {WebGL context} [options.gl] gl context of webgl
     *
     * @exception 
     *
     * @example
     *
     * @private
     */
    function Texture(path, options, callback) {
        this._ready = false;
        this._image = null;
        this._path = path;
        this._gl = options.gl;
        this._texture = null;

        if (ImageCache[path] != null) {
            this._image = ImageCache[path];
            this._texture = createTexture(this._image, this._gl);
            this._ready = true;
            if (callback != null) {
                callback();
            }
            return;
        }
        else {
            init(this, callback);
        }
    }

    defineProperties(Texture.prototype, {
        ready: {
            get: function() {
                return this._ready;
            }
        },
    });

    return Texture;
});

由这个代码,我们可以把原来的texture装载部分,换成:

    var canvas = document.getElementById('my_Canvas');
    var gl = canvas.getContext("experimental-webgl");

    texture = new Texture("./texture/uv.jpg", {gl:gl}, function(tex) {
      material = new Material({vertexShader: shaderCollection['defaultVertex'], fragShader: shaderCollection['defaultFragment'], gl: gl});
      material.setTexture("uSampler", tex);
      textureLoaded = true;
  });

完整的工程代码:

https://github.com/jycgame/WebGL_Tutorial_Texture


化石原创文章,转载请注明来源并保留原文链接



化石原创文章,转载请注明来源并保留原文链接


接上一篇。有了vertex shader和fragment shader源码,我们能通过工具生成相应的javascript文件。这样就能直接用requirejs帮助我们加载,省去很多比较“脏”的异步加载。

不过,光是生成glsl对应的js不够,我们还得使用一个shader的管理部分来让应用方便的使用到对应的shader。所以,工具部分同时生成一个shaderCollection.js,用来放置所有收集到的shader。生成的shaderCollection.js大致长这样:

define([
	'./defaultFragment',
	'./defaultVertex',
	], function(
	defaultFragment,
	defaultVertex,
	){

	var collection = [];

	collection['defaultFragment'] = defaultFragment;
	collection['defaultVertex'] = defaultVertex;

	return collection;
});

也是一个符合requirejs规范的文件。

这样我们就得到了shader部分的大致封装。

工具是C#代码,使用了winform,界面如下:

图1:从glsl生成javascript内容的工具

只能两个可交互的地方:

选择目录,选取一个目录,该目录下的glsl的文件都会被收集(包含子目录)

生成,在每个文件同等目录生成名字一样,后缀为.js的文件。和一个shaderCollection.js文件。

注意:

因为最后的shaderCollection.js模块名的考虑,所有的glsl不能重名,即使是在不同目录下。工具本身不查这个错误。

工具github地址:

https://github.com/jycgame/WebGL_Tutorial_Shader-Tool-Gen

到这里,我们的shader部分差不多。也就是材质的核心就差不多了。我们希望我们使用材质的时候,可以这样使用:

material = new Material({vertexShader: shaderCollection['defaultVertex'], fragShader: shaderCollection['defaultFragment'], gl: gl});

在这个设想下面,我们便有了差不多这样的Material封装:

define(['../core/defineProperties'], function(defineProperties) {

    /**
     * To initialize a material instance.
     *
     * @param {Material} [material] The material to initialize
     *
     * @private
     */
    function init(material) {
        var error;
        var gl = material._gl;

        var vertShader = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vertShader, material.vertexSource);
        gl.compileShader(vertShader);
        error = gl.getError();
        if (error != gl.NO_ERROR) {
            console.log("compile vertex shader error. Error code: " + error);
        }

        var fragShader = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(fragShader, material.fragmentSource);
        gl.compileShader(fragShader);
        error = gl.getError();
        if (error != gl.NO_ERROR) {
            console.log("compile fragment shader error. Error code: " + error);
        }

        var program = gl.createProgram();
        gl.attachShader(program, vertShader); 
        gl.attachShader(program, fragShader);
        gl.linkProgram(program);
        error = gl.getError();
        if (error != gl.NO_ERROR) {
            console.log("link shader error. Error code: " + error);
        }

        material._shaderProgram = program;
    }

    /**
     * class of material
     *
     * @param {Object} [options] Object with the following properties:
     * @param {WebGL context} [options.gl] gl context of webgl
     * @param {String} [options.vertexShader] string contetnt of vertext shader
     * @param {String} [options.fragShader] string content of fragment shader
     *
     * @exception 
     *
     * @example
     *
     * @private
     */
    function Material(options) {
        this._shaderProgram = null;
        this._gl = options.gl;
        this._vertexSource = options.vertexShader;
        this._fragmentSource = options.fragShader;

        init(this);
    }

    Material.prototype.setMatrix = function (name, matrix) {
        var gl = this._gl;

        var loc = gl.getUniformLocation(this._shaderProgram, name);
        gl.uniformMatrix4fv(loc, false, matrix);
    }

    Material.prototype.setTexture = function(name, texture) {
        var gl = this._gl;

        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, texture);
        var uSampler = gl.getUniformLocation(this._shaderProgram, name);
        gl.uniform1i(uSampler, 0);
    }

    defineProperties(Material.prototype, {
        shaderProgram: {
            get: function() {
                return this._shaderProgram;
            }
        },
        vertexSource: {
            get: function() {
                return this._vertexSource;
            }
        },
        fragmentSource: {
            get: function() {
                return this._fragmentSource;
            }
        },

    });

    return Material;
});

上述代码,setMatrix和setTexture方法都是Material开出来的API,还使用了javascript的特性做了一些属性上的设置,比如shaderProgram变量,是个只读的属性。用来给应用层得到该材质封装的Shader程序。

该代码离完整还远远不够,这里只讲个意思。后面随教程深入,会慢慢变完整。

使用这样封装的Material,我们的主代码就变成这样:

define([
    'vertexReorganizer',
    'core/defaultValue',
    'gl-matrix/gl-matrix',
    'shader/shaderCollection',
    'renderer/context',
    'renderer/Material'
    ], function(
        helper,
        defaultValue,
        glMatrix,
        shaderCollection,
        Context,
        Material ) {
    'use strict';

    var vertices;
    var uvs;
    var indices;
    var texture;
    var modelLoaded = false;
    var textureLoaded = false;
    var modelTransform, viewTransform, projectionTransform;
    var modelPositon = glMatrix.vec3.fromValues(0, 0, 0);
    var uv;
    var coord;
    var material;

    //model transform
    modelTransform = glMatrix.mat4.create();
    glMatrix.mat4.fromTranslation(modelTransform, modelPositon);

    function clearCanvas(r, g, b, a) {
        gl.clearColor(49/255, 77/255, 121/255, 1);
        gl.clear(gl.COLOR_BUFFER_BIT);
    }

    function render(timestamp) {
        clearCanvas();

        gl.enable(gl.CULL_FACE);
        gl.cullFace(gl.BACK);

        //如果模型的顶点准备完毕,我们就可以渲染了
        if (modelLoaded &amp;&amp; textureLoaded) {
            gl.useProgram(material.shaderProgram);

            //顶点数据(CPU到GPU)
            var vertex_buffer = gl.createBuffer();
            gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
            gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
            coord = gl.getAttribLocation(material.shaderProgram, "aVertexPosition");
            gl.vertexAttribPointer(coord, 3, gl.FLOAT, false, 0, 0);
            gl.enableVertexAttribArray(coord);
    
            //贴图uv数据 (顶点属性之一)
            var textureCoordBuffer = gl.createBuffer();
            gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
            gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(uvs), gl.STATIC_DRAW);
            uv = gl.getAttribLocation(material.shaderProgram, "aTextureCoord");
            gl.vertexAttribPointer(uv, 2, gl.FLOAT, false, 0, 0);
            gl.enableVertexAttribArray(uv);
    
            //索引数据(CPU到GPU)
            var index_buffer = gl.createBuffer ();
            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, index_buffer);
            gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
    
            gl.viewport(0, 0, canvas.width, canvas.height);
    
            //view transform matrix
            viewTransform = glMatrix.mat4.create();
    
            var cameraPos = glMatrix.vec3.fromValues(0, 0, 50);
            var focalPoint = glMatrix.vec3.fromValues(0, 0, 0);
            var up = glMatrix.vec3.fromValues(0, 1, 0);
    
            glMatrix.mat4.lookAt(viewTransform, cameraPos, focalPoint, up);
    
            //projection transform
            projectionTransform = glMatrix.mat4.create();
            glMatrix.mat4.perspective(projectionTransform, glMatrix.glMatrix.toRadian(15), 1, 0.01, 100);
    
            material.setMatrix("modelTransform", modelTransform);
            material.setMatrix("viewTransform", viewTransform);
            material.setMatrix("projectionTransform", projectionTransform);
            material.setTexture("uSampler", texture);
    
            gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);

            gl.disableVertexAttribArray(uv);
            gl.disableVertexAttribArray(coord);
        }
    }

    // timestamp is the delta time from last time callback called. Use MS.
    function frameUpdate(timestamp) {
        //render
        render(timestamp);

        //schedule the next frame
        requestAnimationFrame(frameUpdate);
    }

    function loadVerticesFromFile(path) {
        let xmlHttpRequest = new XMLHttpRequest();
        xmlHttpRequest.onreadystatechange = function() {
           if (xmlHttpRequest.status == 200 &amp;&amp; xmlHttpRequest.readyState == 4) {
              var txt = xmlHttpRequest.responseText;
 
              var lines = txt.split('\n');
 
              //ignore lines not contain vertices
              var index = 0;
              while(lines[index].indexOf('v ') == -1) {
                 index++;
              }
 
              //1, 读取顶点数据
              vertices = [];
              while(lines[index].indexOf('v ') == 0) {
                 //这里是每一个顶点数据
                 var str = lines[index];
                 var values = str.split(' ');
                   
                 vertices.push(parseFloat(values[1]));
                 vertices.push(parseFloat(values[2]));
                 vertices.push(parseFloat(values[3]));

                 index++;
              }

              //2,读取uv数据
              uvs = [];
              while(lines[index].indexOf('vt ') == 0) {
                 var str = lines[index];
                 var values = str.split(' ');

                 uvs.push(parseFloat(values[1]));
                 uvs.push(parseFloat(values[2]));
                 index++;
              }

              //3,处理法线数据
              var normals = [];
              while(lines[index].indexOf('vn ') == 0) {
                 var str = lines[index];
                 var values = str.split(' ');

                 normals.push(parseFloat(values[1]));
                 normals.push(parseFloat(values[2]));
                 normals.push(parseFloat(values[3]));
                 index++;
              }
 
              while(lines[index].indexOf('f ') == -1) {
                 index++;
              }
                
              //3,处理顶点索引:位置和UV,法线
              while(lines[index].indexOf('f ') == 0) {
                 var line = lines[index];
                 var values = line.split(' ');
 
                 if (values.length == 5) {
                    // first vertex
                    helper.extractAndProcessVertex(vertices, uvs, normals, values[1]);
                    // second vertex
                    helper.extractAndProcessVertex(vertices, uvs, normals, values[2]);
                    // third vertex
                    helper.extractAndProcessVertex(vertices, uvs, normals, values[3]);

                    //第二个三角形
                    // 1st vertex
                    helper.extractAndProcessVertex(vertices, uvs, normals, values[1]);
                    // 2nd vertex
                    helper.extractAndProcessVertex(vertices, uvs, normals, values[3]);
                    // 3rd vertex
                    helper.extractAndProcessVertex(vertices, uvs, normals, values[4]); 
                 }
                 else if(values.length == 4) {
                    // first vertex
                    helper.extractAndProcessVertex(vertices, uvs, normals, values[1]);
                    // second vertex
                    helper.extractAndProcessVertex(vertices, uvs, normals, values[2]);  
                    // third vertex
                    helper.extractAndProcessVertex(vertices, uvs, normals, values[3]);
                 }
                 else {
                    console.log("Impossible!");
                 }
 
                 index++;
              }
 
              vertices = helper.getPositionArray();
              uvs = helper.getUvArray();
              indices = helper.getIndexArray();

              modelLoaded = true;
           }
         
        }
        xmlHttpRequest.open("GET", path);
        xmlHttpRequest.send();         
     }

     function loadTexture(path) {
        const image = new Image();
        image.onload = function() {
           texture = gl.createTexture();
           gl.bindTexture(gl.TEXTURE_2D, texture);
           gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
           gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
           gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
           gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
           gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
           textureLoaded = true;
        }
        image.src = path;
     }

     loadVerticesFromFile("./model/sphere3x.obj");
     loadTexture("./texture/uv.jpg");

    var canvas = document.getElementById('my_Canvas');
    var gl = canvas.getContext("experimental-webgl");

    material = new Material({vertexShader: shaderCollection['defaultVertex'], fragShader: shaderCollection['defaultFragment'], gl: gl});

    // start frame
    frameUpdate();
});

在原来没有这些封装的代码基础上改动而来,属于材质该管的地方,大多已经用上了。看起来代码精简了很多。

完整的代码:

https://github.com/jycgame/WebGL_Tutorial_Material

注意:

原来的一些代码,比如vertexReorganizer.js,我们都改成了符合requirejs规范的。


化石原创文章,转载请注明来源并保留原文链接



化石原创文章,转载请注明来源并保留原文链接


WebGL并没有材质这个概念,但是在更上的层次,比如3D应用、3D引擎中,因为理解的需要,和使用上的方便,往往会需要封装出材质。

材质,这个生活上用来描述物体一部分性质的术语,在我们的3D中,是Shader程序和这段程序需要的相关参数的集合。因为,物体的样子,完全靠渲染表现,而渲染,就是执行shader程序呈现的结果。

这一章我们封装Shader。

首先是shader,把vertex shader和fragment shader分别放在不同的文件里。这样以后可以让能复用的尽量复用。为了方便,shader源代码直接以.glsl文件结尾。考虑到文件加载问题-javascript加载是异步的,但大多shader可以在引擎起始就加载,我们考虑使用requirejs来帮助做这部分工作。但是requirejs本身不能加载非js文件。所以,我们另外写了一个工具,用来将.glsl的shader代码改写成符合requirejs标准的javascript。

比如,一个defaultVertex.glsl文件,它的内容如下:

attribute vec3 aVertexPosition;
attribute vec2 aTextureCoord;
varying highp vec2 vTextureCoord;
uniform mat4 modelTransform;
uniform mat4 viewTransform;
uniform mat4 projectionTransform;

void main(void) {
    gl_Position = projectionTransform * viewTransform * modelTransform * vec4(aVertexPosition, 1.0);
    vTextureCoord = aTextureCoord;
}

使用工具,我们直接生成结果:

define(function() {
    'use strict';

return "attribute vec3 aVertexPosition;\n\
attribute vec2 aTextureCoord;\n\
varying highp vec2 vTextureCoord;\n\
uniform mat4 modelTransform;\n\
uniform mat4 viewTransform;\n\
uniform mat4 projectionTransform;\n\
\n\
void main(void) {\n\
    gl_Position = projectionTransform * viewTransform * modelTransform * vec4(aVertexPosition, 1.0);\n\
    vTextureCoord = aTextureCoord;}";
});

这个符合requirejs标准的javascript,就能直接被require。注意上面的模块,我们直接返回了字符串,字符串内容就是整个shader代码。

这个是shader部分。待续(包含工具代码、实例代码)。


化石原创文章,转载请注明来源并保留原文链接



化石原创文章,转载请注明来源并保留原文链接


写javascript,随着代码规模增大,出现的首要问题便是“怎么模块化”?其他很多语言在设计之初就做好了这个,c、c++用#include,java用import帮助装入一个在其他文件里代码。javascript代码可以放在不同的文件中,只不过在放入html的时候,只是要注意到相互之间的顺序(如果使用过layabox等引擎,就知道引擎发布出来的html,我们经常需要手动去改,问题就在这里)。

这里我们来用requirejs来解决这个问题。requirejs是一个单独的js文件,搜索下载即可。

用法:      

1、主html中使用requirejs

<script src="script/require.js" data-main="script/main"></script>

2、修改自己的javascript文件,成为符合requirejs格式的

define(['dependency1, dependency2'], function(module1FromDependency1, module2FromDependency2) {
 //our class define
 //return the class
});

格式用define开始,中括弧中放置各个依赖的javascript的路径,requirejs会加载这些javascript,成为对应的后面function中的参数。在function中,我们和平常一样写javascript,只不过要return出相应的东西成为模块(这个看我本章对应的例子比较容易)。

更详细的请搜索,这类文章非常容易得到,而且高品质的不少。

3、在第1步的脚本申明中,用data-main告诉requirejs我们的入口脚本。这个在上面的示例中已经可以看到。

本章对应的例子程序路径在这里:

https://github.com/jycgame/WebGL_Tutorial_47

我们以后可能会经常遇到的两者写法分别在defaultValue.js中,这个里面模块返回一个方法:

define(function() {
    'use strict';

    function defaultValue(a, b) {
        if (a !== undefined &amp;&amp; a !== null) {
            return a;
        }

        return b;
    }

    return defaultValue;
});

还有一种写法如Context.js,

define(['../core/defineProperties'
    ], function(
        defineProperties) {
    'use strict';

    function Context(canvas, options) {
        // members

        // functions
        this.originalCanvas = canvas;
    }

    defineProperties(Context.prototype, {
        id : {
            get : function() {
                return 100;
            }
        }
    });

    return Context;
});

注:代码中几个都使用了Cesium的设计。


化石原创文章,转载请注明来源并保留原文链接