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