2018年10月29日 星期一

理解 html 上的 canvas 直接透過 webgl API 繪 3D 圖

<html><head><meta charset="utf-8"/></head><body><script>
// webGL z-axis 稱為 depth, 以螢幕當基準點 0, 螢幕後面是負值, 前面是正, 螢幕左右是 x 軸, 上下是 y 軸, 螢幕中心是 (0, 0, 0)
// webGL 用的座標稱為卷座標 (clip coordinate), 座標值 x, y, z 都介於 -1 與 1 之間的浮點數
// 物件以原點(0,0,0)當參考點, 稱為世界座標 (world coordinate)
// Vertices 是 3D 物件的頂點座標
// 世界座標可以經由矩陣轉換成 webGL 的卷座標: clip_coordinate = Pmatrix * Vmatix * Mmatrix * world_coordinate
//   Mmatrix 是物體移動的矩陣,讓物件可以在世界中自由變換位置甚至旋轉及放大(Translate, scale, rotate)
//   Vmatrix 可以當成觀景矩陣,如果將相機觀景點當成原點,以相對觀點來看,等同將世界座標的零點座標(0,0,0)逆轉換(invere transform)也就是移動世界
//   Pmatrix 是投射矩陣, 將世界座標最後投射到 GL 用的 -1 到 1 之間的繪圖座標點, 同時可以限定在 canvas 的畫布之內
// vertex and index buffer 用於描述模型的幾何結構, mesh 稱為網目(面), 一般會用三角形的碎形面(fragment)建構物體的表面
// ESSL : Embedded System Shader Language 內嵌系統渲染程式語言
// GLSL : OpenGL ES Shader Language 必須包括點及碎面的渲染程式(Vertex and Fragment shader program)
// Shader Language 用來定義 vertices, transformations, materials, lights, camera 的交互作用下產生的一幅影像
// vertex Shader 點渲染作用於 3D 座標點及線上 , 碎面渲染(Fragment Shader)作用於充實物件表面
// drawElements(),drawArrays() 單元或陣列繪畫
// 參考資料:
//      1.  https://webglfundamentals.org/webgl/lessons/webgl-drawing-multiple-things.html
//      2.  https://www.tutorialspoint.com/webgl/
//      3.  https://www.khronos.org/registry/OpenGL/specs/gl/GLSLangSpec.1.10.pdf
// Init time:
//      1. create shader program, lookup attribute and uniform location
//      2. create buufers and upload data
//      3. create textures and upload data
// Rander time:
//      1. useProgram
//      2. setup attribute and uniform
//      3. drawArrays or drawElements
// GLSL 所有變數使用前必須先宣告資料型態, 基本資料型態:
// void, float, bool, int, vect2, vect3, vect4, bvect2, bvect3, bvect4, ivect2, ivect3, ivect4, mat2, mat3, mat4, sampler1D, sampler2D, sampler3D, samplerCube, sampler1Dshadow, sampler2Dshadow
// void 是忽視的資料, float 是浮點數資料, bool 是布林數資料, int 是整數資料  (V FBI)
// float 可以使用 E 或 e 在科學記號表示式中作為 10 的指數(10^), 句點分離整數與小數
// bool 僅可以用 true, false 賦值
// int 整數前置可以用 0 代表 8 進制整數, 用 0x 或 0X 代表 16 進制整數, 否則一般就是 10 進制整數
// vect 是浮點數的向量, bvect 是布林數的向量, ivect 是整數值的向量
// 方陣是以行為主(column-major order),也就是向量陣列的方陣,  mat2 是 2x2 方陣, mat3 是 3x3 方陣, mat4 是 4x4 方陣
// 資料整合可以用 struct newType { } 群聚起來成為新的資料型態(newType), 往後資料宣告時就可加以利用
// 資料型態修飾詞(Type Qualifiers) 可以用 const, attribute, uniform, varying, in, out, inout 關鍵字來修飾變數
// 在函式外部宣告的變數, 它是整體區域(Global scope)變數, 僅可以加上 const, attribute, uniform, varying 其中之一的修飾詞
// 函式輸出入參數只能用 in, out, inout, const 其中之一的修飾詞
// const 顧名思義就是不能修改的常數
// attribute 修飾詞用於將變數當成參數傳遞(像是將 vertices 從 OpenGL 一一傳給 vertext shader)時來使用, 對 vertex shader 而言它是常數
// attirbute 只可以用來修飾 float, vect, mat 等浮點數, 它不能用於修飾陣列(array)或是結構(struct)的資料型態.
// uniform 作用於整體區域的宣告, 基本上它是唯讀的, 可以直接透過 API 或是間接經由 OpenGL 初始化.
// varying 提供 vertex shadow 與 fragement shadow 及固定函式之間作為資料交換介面使用
const π = Math.PI;
let tan = Math.tan;
let cos = Math.cos;
let sin = Math.sin;
class matriX4 { // 4x4 方陣
    constructor()   { this.m = [1,0,0,0,  0,1,0,0,  0,0,1,0,  0,0,0,1];  }
    translate(x,y,z){ // bind this with objet I4
        this.m[12] += x;
        this.m[13] += y;
        this.m[14] += z;
        return this;
    }
    scale(sx,sy,sz) { // scale depend on sx,sy,sz)
        this.m[0] *= sx;
        this.m[5] *= sy;
        this.m[10]*= sz;
        return this;
    }
    translatex(s)   { this.m[12] += s; return this;}
    translatey(s)   { this.m[13] += s; return this;}
    translatez(s)   { this.m[14] += s; return this;}
    scalex(s)       { this.m[0]  *= s; return this;}
    scaley(s)       { this.m[5]  *= s; return this;}
    scalez(s)       { this.m[10] *= s; return this;}
    rotatex(θ)      {
        let c = cos(θ);
        let s = sin(θ);
        let m = this.m;
        let mv1 = m[1], mv5 = m[5], mv9 = m[9];
        m[1]  = m[1]*c - m[2]*s;
        m[5]  = m[5]*c - m[6]*s;
        m[9]  = m[9]*c - m[10]*s;
        m[2]  = m[2]*c + mv1*s;
        m[6]  = m[6]*c + mv5*s;
        m[10] = m[10]*c+ mv9*s;
        return this;
    }   
    rotatey(θ)      {
        let c = cos(θ);
        let s = sin(θ);
        let m = this.m;
        let mv0 = m[0], mv4 = m[4], mv8 = m[8];
        m[0]  = c*m[0] + s*m[2];
        m[4]  = c*m[4] + s*m[6];
        m[8]  = c*m[8] + s*m[10];
        m[2]  = c*m[2] - s*mv0;
        m[6]  = c*m[6] - s*mv4;
        m[10] = c*m[10]- s*mv8;
        return this;
    }      
    rotatez(θ)      {
        let c = cos(θ);
        let s = sin(θ);
        let m = this.m;
        let mv0 = m[0], mv4 = m[4], mv8 = m[8];
        m[0] = c*m[0] - s*m[1];
        m[4] = c*m[4] - s*m[5];
        m[8] = c*m[8] - s*m[9];
        m[1] = c*m[1] + s*mv0 ;
        m[5] = c*m[5] + s*mv4 ;
        m[9] = c*m[9] + s*mv8 ;
        return this;
    }
    projector(θ, near, far, width, height) {
        if (near < 1e-4) near = 1e-4;   
        let depth = far - near;
        let factor = tan((π-θ)/2); //  0< θ < π
        this.width = width;
        this.height= height;
        this.m[0]  = factor*height/width;
        this.m[5]  = factor;
        this.m[10] = -(near+far)/depth;// width > height
        this.m[11] = -1;
        this.m[14] = -2*near*far/depth;
        this.m[15] = 0 ;
        return this;
    }
}
class canvas3D{ 
    constructor(width,height,depth) {
        let div    = document.createElement("div");
        let canvas = document.createElement("canvas");   
        this.context = canvas.getContext('webgl'); 
        this.border = 8;
         document.body.style= "background-color: #080808;";
        canvas.style.border = this.border+"px solid";
        document.body.appendChild(div);              
        div.align="center";   
         div.appendChild(canvas);
        canvas.width = width;
        canvas.height= height;
        this.width   = canvas.width;
        this.height  = canvas.height;
        this.depth   = depth;     
        this.shaderGPU= 0;
        this.program = this.context.createProgram();
    }   
    set assemble(str)   {
        this.context.shaderSource(this.shader, str);
        this.context.compileShader(this.shader);
        this.context.attachShader(this.program, this.shader);
        if( ++this.shaderGPU == 2 ) {
            this.context.linkProgram(this.program);
            this.context.useProgram(this.program);
            this.context.enable(this.context.DEPTH_TEST);
            this.context.viewport(0, 0, this.width, this.height);
            console.log("Shader Run: "+this.shaderGPU)
        }
    }
    set vertext(code)   {
        this.shader = this.context.createShader(this.context.VERTEX_SHADER);
        this.assemble = code;
    }
    set fragment(code)  {
        this.shader = this.context.createShader(this.context.FRAGMENT_SHADER);
        this.assemble = code;
    }   
    set scale(name)     { // remember the scalename to be used in projector
        let location = this.context.getUniformLocation(this.program, name);
        this.context.uniform3f(location, this.width, this.height, this.depth);
        this.scalename = name;
    }
    uniform(name)       { // use fat arrow function ()=>{} to bind this with canvas3D object
        let location = this.context.getUniformLocation(this.program, name);
        return { transfer:(m) => { this.context.uniformMatrix4fv(location, false, m); } }
    }
    attribute(name)     { // transfer the attributes one by one
        let location = this.context.getAttribLocation(this.program, name);
        return { transfer:(size) => {
                this.context.vertexAttribPointer(location, size, this.context.FLOAT, false, 0, 0);
                this.context.enableVertexAttribArray(location);
            }
        }
    }
    adddata(array)      {
        let buffer = this.context.createBuffer();
        this.context.bindBuffer(this.context.ARRAY_BUFFER, buffer);
        this.context.bufferData(this.context.ARRAY_BUFFER, new Float32Array(array), this.context.STATIC_DRAW);
        return buffer;
    }
    indexbuffer(array)  {
        this.index = this.context.createBuffer();
        this.context.bindBuffer(this.context.ELEMENT_ARRAY_BUFFER, this.index);
        this.context.bufferData(this.context.ELEMENT_ARRAY_BUFFER, new Uint16Array(array), this.context.STATIC_DRAW);
        this.length = array.length;
        return this;
    }
    pointbuffer(array)  {
        this.buffer = this.adddata(array);
        this.context.bindBuffer(this.context.ARRAY_BUFFER, this.buffer);
        return this;
    }
    colorbuffer(array)  {
        this.color = this.adddata(array);
        this.context.bindBuffer(this.context.ARRAY_BUFFER, this.color);
        return this;
    }  
    get draw()      {
        this.context.bindBuffer(this.context.ELEMENT_ARRAY_BUFFER, this.index);
        this.context.drawElements(this.context.TRIANGLES , this.length , this.context.UNSIGNED_SHORT,0);
    }
    get clear()         { this.context.clear(this.context.COLOR_BUFFER_BIT | this.context.DEPTH_BUFFER_BIT); return this; }       
    projector(θ, near, far) {
        return  new matriX4().projector(θ, near, far, this.width, this.height);
    }   
}
let vertex = [
            -1,-1,-1,
             1,-1,-1,
             1, 1,-1,
            -1, 1,-1,
            -1,-1, 1,
             1,-1, 1,
             1, 1, 1,
            -1, 1, 1,
            -1,-1,-1,
            -1, 1,-1,
            -1, 1, 1,
            -1,-1, 1,
             1,-1,-1,
             1, 1,-1,
             1, 1, 1,
             1,-1, 1,
            -1,-1,-1,
            -1,-1, 1,
             1,-1, 1,
             1,-1,-1,
            -1, 1,-1,
            -1, 1, 1,
             1, 1, 1,
             1, 1,-1,
];
let colors = [
            5,3,7, 5,3,7, 5,3,7, 5,3,7,
            1,1,3, 1,1,3, 1,1,3, 1,1,3,
            0,0,1, 0,0,1, 0,0,1, 0,0,1,
            1,0,0, 1,0,0, 1,0,0, 1,0,0,
            1,1,0, 1,1,0, 1,1,0, 1,1,0,
            0,1,0, 0,1,0, 0,1,0, 0,1,0
]; 
let indices= [
            0,1,2, 0,2,3, 4,5,6, 4,6,7,
            8,9,10, 8,10,11, 12,13,14, 12,14,15,
            16,17,18, 16,18,19, 20,21,22, 20,22,23
];
var my = new canvas3D(500, 500, 500);
my.vertext     = 'attribute vec3 position;attribute vec3 color;'+
                 'uniform mat4 Pmatrix;uniform mat4 Vmatrix;uniform mat4 Mmatrix;'+
                 'varying vec3 vColor;'+
                 'void main(void) { gl_Position  = Pmatrix*Vmatrix*Mmatrix*vec4(position, 1.);vColor = color;}';
my.fragment    = 'precision mediump float;'+
                 'varying vec3 vColor;'+
                 'void main(void) { gl_FragColor = vec4(vColor, 1.);}';
let abs = (x) => { return x < 0 ? -x:x;}
let z = -10;            // back camera off in z direction to see target
let θ = π/2 ;               // field of view angle  = 90 degree for the camera
let near = 1;               // nearest distance between camera and target in z direction
let far  = near + abs(z);   // farest distance between camera and target in z direction
let Δz = z;                 // camera back off
let Δy = abs(Δz)*tan(θ/2);  // camera pull up
my.indexbuffer(indices);
my.colorbuffer(colors).attribute('color').transfer(3);          // 3 per time
my.pointbuffer(vertex).attribute('position').transfer(3);          // 3 per time
my.uniform("Pmatrix").transfer(my.projector(θ, near, far).m);   // perspective project
my.uniform("Vmatrix").transfer(new matriX4().translate(0,Δy,Δz).rotatex(θ).m);// camera matrix
let mat4GPU = my.uniform("Mmatrix");                            // get GPU mat4 location
let matrix  = new matriX4().rotatex(π/4).rotatey(π/4);          // matrix of the target to use in animatiton
    let Δθ    = π/5;
    let Δs    = 1;
    var animatiton = function() {     
            mat4GPU.transfer(matrix.translatez(Δs).rotatez(Δθ).m);
            my.clear.draw;         
            z += Δs ;
            console.log(abs(z)+":"+abs(Δz))  ; 
            if( abs(z) <= abs(Δz) ) setTimeout(animatiton, 500);
    }
    animatiton();   
</script></body></html>        

沒有留言: