webgl-rotating-teapot-example

Index Commits Files Refs README
main.js (9046B)
   1 let gl, canvas, glProgram, style;
   2 let teapotMesh, positionBuffer, indexBuffer, wireframeBuffer, normalBuffer;
   3 
   4 const TARGET_ASPECT = 1.0;
   5 const WIREFRAME = true;
   6 
   7 function makeShader(source, type) {
   8     let shader = gl.createShader(type);
   9     gl.shaderSource(shader, source);
  10     gl.compileShader(shader);
  11     return shader;
  12 }
  13 
  14 async function setupShaders() {
  15     glProgram = gl.createProgram();
  16 
  17     const [vertexShaderSrc, fragmentShaderSrc] = await Promise.all([
  18         fetch('main.vert').then(r => r.text()),
  19         fetch('main.frag').then(r => r.text())
  20     ]);
  21 
  22     const vertexShader = makeShader(vertexShaderSrc, gl.VERTEX_SHADER);
  23     gl.attachShader(glProgram, vertexShader);
  24 
  25     const fragmentShader = makeShader(fragmentShaderSrc, gl.FRAGMENT_SHADER);
  26     gl.attachShader(glProgram, fragmentShader);
  27 
  28     gl.linkProgram(glProgram);
  29     gl.useProgram(glProgram);
  30 }
  31 
  32 async function setupBuffers() {
  33     const teapotText = await fetch('teapot.obj').then(r => r.text());
  34     teapotMesh = parseOBJ(teapotText);
  35 
  36     positionBuffer = gl.createBuffer();
  37     gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  38     gl.bufferData(gl.ARRAY_BUFFER, 
  39         new Float32Array(teapotMesh.positions), gl.STATIC_DRAW);
  40 
  41     normalBuffer = gl.createBuffer();
  42     gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
  43     gl.bufferData(gl.ARRAY_BUFFER,
  44         new Float32Array(teapotMesh.normals), gl.STATIC_DRAW);
  45 
  46     if(WIREFRAME == true) {
  47         wireframeBuffer = gl.createBuffer();
  48         gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, wireframeBuffer);
  49         gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,
  50             new Uint16Array(teapotMesh.wireframeIndices), gl.STATIC_DRAW);
  51     } else {
  52         indexBuffer = gl.createBuffer();
  53         gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  54         gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,
  55             new Uint16Array(teapotMesh.indices), gl.STATIC_DRAW);
  56     }
  57 }
  58 
  59 function generateWireframeIndices(triangleIndices) {
  60     const edges = new Set();
  61     const wireframeIndices = [];
  62 
  63     for (let i = 0; i < triangleIndices.length; i += 3) {
  64         const a = triangleIndices[i];
  65         const b = triangleIndices[i + 1];
  66         const c = triangleIndices[i + 2];
  67 
  68         function addEdge(i1, i2) {
  69             const edge = i1 < i2 ? `${i1}-${i2}` : `${i2}-${i1}`;
  70             if (!edges.has(edge)) {
  71                 edges.add(edge);
  72                 wireframeIndices.push(i1, i2);
  73             }
  74         }
  75 
  76         addEdge(a, b);
  77         addEdge(b, c);
  78         addEdge(c, a);
  79     }
  80 
  81     return wireframeIndices;
  82 }
  83 
  84 function parseOBJ(text) {
  85     const positions = [];
  86     const texcoords = [];
  87     const normals = [];
  88     const indices = [];
  89 
  90     const finalPositions = [];
  91     const finalTexcoords = [];
  92     const finalNormals = [];
  93     const indexMap = new Map();
  94 
  95     const lines = text.split('\n');
  96 
  97     for (const line of lines) {
  98         const parts = line.trim().split(/\s+/);
  99         if (parts.length === 0) continue;
 100 
 101         switch (parts[0]) {
 102             case 'v':
 103                 positions.push(parts.slice(1).map(Number));
 104                 break;
 105             case 'vt':
 106                 texcoords.push(parts.slice(1).map(Number));
 107                 break;
 108             case 'vn':
 109                 normals.push(parts.slice(1).map(Number));
 110                 break;
 111             case 'f':
 112                 for (let i = 1; i <= 3; i++) {
 113                     const [v, vt, vn] = parts[i].split('/').map(str => parseInt(str, 10) - 1);
 114                     const key = `${v}/${vt}/${vn}`;
 115                     if (!indexMap.has(key)) {
 116                         finalPositions.push(...positions[v]);
 117                         if (texcoords[vt]) finalTexcoords.push(...texcoords[vt]);
 118                         if (normals[vn]) finalNormals.push(...normals[vn]);
 119                         indexMap.set(key, finalPositions.length / 3 - 1);
 120                     }
 121                     indices.push(indexMap.get(key));
 122                 }
 123                 break;
 124         }
 125     }
 126 
 127     const wireframeIndices = generateWireframeIndices(indices);
 128     return {
 129         positions: finalPositions,
 130         texcoords: finalTexcoords,
 131         normals: finalNormals,
 132         indices: indices,
 133         wireframeIndices: wireframeIndices,
 134     };
 135 }
 136 
 137 function drawScene() {
 138     const aPos = gl.getAttribLocation(glProgram, "a_pos");
 139     const aNormal = gl.getAttribLocation(glProgram, "a_normal");
 140 
 141     gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
 142     gl.enableVertexAttribArray(aPos);
 143     gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, 0, 0);
 144 
 145     gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
 146     gl.enableVertexAttribArray(aNormal);
 147     gl.vertexAttribPointer(aNormal, 3, gl.FLOAT, false, 0, 0);
 148 
 149     if(WIREFRAME == true) {
 150         gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, wireframeBuffer);
 151     } else {
 152         gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
 153     }
 154 }
 155 
 156 function onResize() {
 157     canvas.width = window.innerWidth;
 158     canvas.height = window.innerHeight;
 159     const aspect_ratio = canvas.width / canvas.height;
 160 
 161     let width, height;
 162     if (aspect_ratio > TARGET_ASPECT) {
 163         height = window.innerHeight;
 164         width = height * TARGET_ASPECT;
 165     } else {
 166         width = window.innerWidth;
 167         height = width / TARGET_ASPECT;
 168     }
 169 
 170     canvas.style.width = width + 'px';
 171     canvas.style.height = height + 'px';
 172 
 173     gl.viewport(0, 0, canvas.width, canvas.height);
 174 }
 175 
 176 function parseCSSColor(cssColor) {
 177     const match = cssColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
 178 
 179     if (!match) {
 180         return [0, 0, 0, 1];
 181     }
 182 
 183     const r = parseInt(match[1], 10) / 255;
 184     const g = parseInt(match[2], 10) / 255;
 185     const b = parseInt(match[3], 10) / 255;
 186     const a = match[4] !== undefined ? parseFloat(match[4]) : 1;
 187 
 188     return [r, g, b, a];
 189 }
 190 
 191 let timeLast, frames;
 192 function render() {
 193     const time = performance.now()/1000; // in ms
 194 
 195     if((time - timeLast) > 1) {
 196         const fps_div = document.getElementById("fps");
 197         fps_div.innerHTML = frames;
 198         timeLast = performance.now()/1000;
 199         frames = 0;
 200     }
 201 
 202     frames++;
 203 
 204     const uTransform = gl.getUniformLocation(glProgram, "u_transform");
 205     const uNormalMatrix = gl.getUniformLocation(glProgram, "u_normal_matrix");
 206 
 207     const uLightDirection = gl.getUniformLocation(glProgram, "u_light_dir");
 208     const uLightColor = gl.getUniformLocation(glProgram, "u_light_color");
 209     const uObjectColor = gl.getUniformLocation(glProgram, "u_object_color");
 210     const uAmbientLightColor = gl.getUniformLocation(glProgram, "u_ambient_color");
 211 
 212     gl.uniform3fv(uLightDirection, glMatrix.vec3.normalize([], [0.0, 1.0, 0.0]));
 213     gl.uniform3fv(uLightColor, [1.0, 1.0, 1.0]);
 214     gl.uniform3fv(uObjectColor, [1.0, 0.0, 0.0]);
 215     gl.uniform3fv(uAmbientLightColor, [0.10, 0.10, 0.10]);
 216 
 217     const modelMatrix = glMatrix.mat4.create();
 218 
 219     glMatrix.mat4.translate(modelMatrix, modelMatrix, [-.01, 0, 0]);
 220     glMatrix.mat4.rotate(modelMatrix, modelMatrix, -time*0.250, [0, 1, 0]);
 221     glMatrix.mat4.translate(modelMatrix, modelMatrix, [0.08, 0, 0]);
 222     glMatrix.mat4.scale(modelMatrix, modelMatrix, [.075, .075, .075]);
 223 
 224     const viewMatrix = glMatrix.mat4.create();
 225     glMatrix.mat4.lookAt(viewMatrix,
 226         [0, 4, 4], // position
 227         [0, 0, 0], // target
 228         [0, 1, 0]  // up vector
 229     );
 230 
 231     const projectionMatrix = glMatrix.mat4.create();
 232     glMatrix.mat4.perspective(projectionMatrix,
 233         Math.PI/5, TARGET_ASPECT, 0.1, 100.0);
 234 
 235     const modelViewMatrix = glMatrix.mat4.create();
 236     const modelViewProjectionMatrix = glMatrix.mat4.create();
 237     glMatrix.mat4.multiply(modelViewMatrix, viewMatrix, modelMatrix);
 238     glMatrix.mat4.multiply(modelViewProjectionMatrix, projectionMatrix, modelViewMatrix);
 239 
 240     const normalMatrix = glMatrix.mat3.create();
 241     glMatrix.mat3.normalFromMat4(normalMatrix, modelMatrix);
 242 
 243     gl.uniformMatrix3fv(uNormalMatrix, false, normalMatrix);
 244     gl.uniformMatrix4fv(uTransform, false, modelViewProjectionMatrix);
 245 
 246     const [r, g, b, a] = parseCSSColor(style.backgroundColor);
 247     gl.clearColor(r, g, b, a);
 248     gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
 249 
 250     if(WIREFRAME == true) {
 251         gl.drawElements(gl.LINES, teapotMesh.wireframeIndices.length, gl.UNSIGNED_SHORT, 0);
 252     } else {
 253         gl.drawElements(gl.TRIANGLES, teapotMesh.indices.length, gl.UNSIGNED_SHORT, 0);
 254     }
 255 
 256     requestAnimationFrame(render);
 257 }
 258 
 259 function main() {
 260     canvas = document.getElementById("canvas");
 261     style = getComputedStyle(document.querySelector('body'));
 262 
 263     gl = canvas.getContext("webgl");
 264     gl.disable(gl.CULL_FACE);
 265     gl.enable(gl.DEPTH_TEST);
 266     gl.depthMask(true);
 267 
 268     timeLast = performance.now()/1000;
 269     frames = 0;
 270 
 271     onResize();
 272     window.addEventListener('resize', onResize);
 273 
 274     (async () => {
 275         await setupShaders();
 276         console.log("[LOG]: Shaders done");
 277 
 278         await setupBuffers();
 279         console.log("[LOG]: Buffers done");
 280 
 281         drawScene();
 282         requestAnimationFrame(render);
 283     })();
 284 }
 285 
 286 main();