added webgl basics

This commit is contained in:
Fedor Katurov 2022-11-27 19:29:56 +06:00
parent 509e2b9406
commit 189028c533
4 changed files with 381 additions and 0 deletions

View file

@ -0,0 +1,243 @@
## Helpful documentation
- https://open.gl/
- This youtube series: [Basics](https://www.youtube.com/watch?v=kB0ZVUrI4Aw), [Making a cube, applying transformations](https://www.youtube.com/watch?v=3yLL9ADo-ko), [Texturing](https://www.youtube.com/watch?v=hpnd11doMgc&t=52s)
## Vertices and Indices
**Vertices** are points with specific coordinates `{x,y,z}` in a 3D-space. We can build any figure by connecting **vertices** in triangles with **indices**.
## Shaders
Shaders are functions, written in C, which describe how to draw and color polygons to Graphic Card.
[Vertex Shaders](Vertex%20Shaders.md) describe **vertice** positions, so Graphic Card can position them by connecting with **indices** and project to 2D canvas.
[Fragment Shaders](Fragment%20Shaders.md) describe the way polygons should be colored by assigning colors to Vertices or by applying textures to polygons.
Shaders can have parameters passed from Javascript code (`uniforms`, `varyings` and `attributes`). [Fragment Shaders](Fragment%20Shaders.md) can also access data from [Vertex Shaders](Vertex%20Shaders.md) (that ones called `varyings`).
![](https://open.gl/media/img/c2_pipeline.png)
## Program
Program, as far as I understand, is a scene, that's described with **Vertices**, **Indices**, specific [Vertex Shaders](Vertex%20Shaders.md) and [Fragment Shaders](Fragment%20Shaders.md).
## Applying transformations
The best way to change positions inside [Vertex Shaders](Vertex%20Shaders.md) or color in [Fragment Shaders](Fragment%20Shaders.md) is to pass parameters (also called `uniforms` and `varyings`).
Read about that at [open.gl](https://open.gl/transformations) and at [Vertex Shaders](Vertex%20Shaders.md).
## Source code with explanations
```typescript
// render-a-cube.ts
import { createShader } from "./create-shader";
import vxShader from "./vertex.glsl?raw";
import fgShader from "./fragment.glsl?raw";
const canvas = document.getElementyId('view');
const ctx = canvas.getRenderingContext('webgl');
// should be put inside requestAnimationFrame
drawCube(ctx)();
function drawCube (
gl: WebGL2RenderingContext,
width: number,
height: number
) {
// Initializing viewport
gl.viewport(0, 0, width, height);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
const prg = gl.createProgram();
if (!prg) {
throw new Error("Can't init programm");
}
// Setting up VERTEX and FRAGMENT shaders
const vx = createShader(gl, vxShader, gl.VERTEX_SHADER);
gl.attachShader(prg, vx);
const fx = createShader(gl, fgShader, gl.FRAGMENT_SHADER);
gl.attachShader(prg, fx);
gl.linkProgram(prg);
if (!gl.getProgramParameter(prg, gl.LINK_STATUS)) {
throw new Error("Could not initialise shaders");
}
// Cube's vertices Array<[x,y,z]>, 8 items
const vertices = [
-1, -1, -1, // 0
1, -1, -1, // 1
1, 1, -1, // 2
-1, 1, -1, // 3
-1, -1, 1, // 4
1, -1, 1, // 5
1, 1, 1, // 6
-1, 1, 1, // 7
];
// indices, that form triangles, that form cube sides
const indices = [
2, 1, 0, // side 0 (first triangle)
0, 3, 2, // side 0 (second triangle)
0, 4, 7, // side 1 (first triangle)
7, 3, 0, // side 1 (second triangle)
0, 1, 5, // ...
5, 4, 0,
1, 2, 6,
6, 5, 1,
2, 3, 7,
7, 6, 2,
4, 5, 6,
6, 7, 4,
];
// createe a vertex buffer and bind vertices to it
const squareVertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// create a vertex buffer and bind indices to it
const squareIndexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareIndexBuffer);
gl.bufferData(
gl.ELEMENT_ARRAY_BUFFER,
new Uint16Array(indices),
gl.STATIC_DRAW
);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
// initial drawing
gl.clearColor(0.0, 0.0, 0.0, 0.0);
gl.enable(gl.DEPTH_TEST);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.viewport(0, 0, width, height);
// bind squareVertexBuffer as vertex positions buffer
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexBuffer);
// send every 3 bytes from squareVertexBuffer as {x,y,z} for each verticle
gl.vertexAttribPointer(
gl.getAttribLocation(prg, "aVertexPosition"),
3, // 3 bytes-long
gl.FLOAT,
false, // don't normalize (int to float)
0,
0
);
// send vertice buffer as `aVertexPosition` attribute inside vertex shader
gl.enableVertexAttribArray(
gl.getAttribLocation(prg, "aVertexPosition")
);
let i = 0;
let speed = 0.01;
// that's the main rendering callback
return () => {
gl.useProgram(prg);
const scale = i * 0.25 + 0.25;
// used for scaling inside Vertex Shader
gl.uniform1f(gl.getUniformLocation(prg, "slide"), scale);
// GL Screen is square, so we need to fix it's aspect ration
gl.uniform1f(gl.getUniformLocation(prg, "aspect"), height / width);
gl.bindBuffer(
gl.ELEMENT_ARRAY_BUFFER,
squareIndexBuffer
);
gl.drawElements(
gl.TRIANGLES,
indices.length,
gl.UNSIGNED_SHORT,
0,
);
if (i > 1 || i < 0) {
speed = -speed;
}
i += speed;
};
};
```
## Shader compiler
```typescript
// create-shader.ts
export const createShader = (
gl: WebGL2RenderingContext,
sourceCode: string,
type: number, // gl.VERTEX_SHADER or gl.FRAGMENT_SHADER
) => {
const shader = gl.createShader(type);
if (!shader) {
throw new Error(`Can't init shader`);
}
gl.shaderSource(shader, sourceCode);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const info = gl.getShaderInfoLog(shader);
throw `Could not compile WebGL program. \n\n${info}`;
}
return shader;
};
```
## Vertex Shader Example
Read more at [Vertex Shaders](Vertex%20Shaders.md)
```c
// current vertice position {x,y,z,w}
attribute vec4 aVertexPosition;
// final vertice position with all transformations applied,
// that will be passed to Fragment Shader
varying vec4 v_positionWithOffset;
// Parameters passed from Javascript loop
uniform float slide;
uniform float aspect;
void main(){
// float array of 4 elements, same as [slide,slide,slide,1]
vec4 scale=vec4(vec3(slide),1);
// float array of 4 elements, same as [aspect,1,1,1]
vec4 aspectRatioFix=vec4(aspect,vec3(1));
// vertice position, multiplied with matrices of scale and aspect ratio
gl_Position=aVertexPosition*scale*aspectRatioFix,
// vertice offset, that will be passed to fragment shader
v_positionWithOffset=gl_Position+vec4(1,1,1,1);
}
```
## Fragment Shader Example
Read more at [Fragment Shaders](Fragment%20Shaders.md).
```c
precision highp float;
// parameter from Vertex Shader
varying vec4 v_positionWithOffset;
void main(void){
// color, attached to current verticle {r,g,b,alpha}
// same a[
// v_positionWithOffset.x,
// v_positionWithOffset.y,
// v_positionWithOffset.z,
// 1
// ]
gl_FragColor=vec4(v_positionWithOffset.xyz,1);
}
```

View file

@ -0,0 +1,25 @@
Fragment shaders describe ==how polygons are painted== (or textured). Read [Basics of WebGL (Drawing a Cube)](Basics%20of%20WebGL%20(Drawing%20a%20Cube).md) first.
## Sample fragment vertex
Parameters could be passed here as written at [Vertex Shaders](Vertex%20Shaders.md).
```c
precision highp float;
// parameter from Vertex Shader
varying vec4 v_positionWithOffset;
// parameters passed from Javascript loop
uniform float slide;
uniform float aspect;
void main(void){
// color, attached to current verticle {r,g,b,alpha}
// same a[
// v_positionWithOffset.x,
// v_positionWithOffset.y,
// v_positionWithOffset.z,
// 1
// ]
gl_FragColor=vec4(v_positionWithOffset.xyz,1);
}```

View file

@ -0,0 +1,68 @@
Rendering items on #canvas in main loop ==might cause interface freezes==, preventing render process from executing properly by flooding execution stack with operations.
To handle it properly, we can start separate #worker thread, that will handle rendering in its own event loop, so that won't affect the source tab's event loop.
Workers can have access to [Transferrable Objects](https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects) from main thread by receiving memory address. ==That's a lot faster than cloning==. In this case [ImageBitmap](https://developer.mozilla.org/en-US/docs/Web/API/ImageBitmap) is transferrable.
Code for the main thread component:
```typescript
// main.ts
const instance = new Worker('./render-worker.ts');
const canvas = document.getElementById('view');
// attaching event listener to worker
instance.onmessage = (event: MessageEvent) => {
const ctx = canvas?.getContext("2d");
if (!ctx) {
throw new Error(`Can't get 2D context`);
}
ctx.drawImage(event.data as ImageBitmap, 0, 0);
}
// sending canvas contents to worker
const renderInCanvas = () => {
if (!canvas.current) {
return;
}
createImageBitmap(canvas.current).then(image => {
instance.postMessage(image, { transfer: [image] })
});
};
```
Worker code:
```typescript
// render-worker.ts
export default () => {
self.onmessage = (message: MessageEvent) => {
const data = message.data;
if (!(data instanceof ImageBitmap)) {
throw new Error('Received unknown data')
}
// OffscreenCanvas can be set up inside workers
const canvas = new OffscreenCanvas(data.width, data.height);
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error(`Can't get 2D context`);
}
ctx.drawImage(data, 0, 0);
// That will block execution inside worker for
// a couple of seconds
doHardRenderingStuffHere(ctx, data.width, data.height);
// Sending resulting image back to main thread
createImageBitmap(canvas).then((image) => {
self.postMessage(image, { transfer: [image] });
});
};
};
```

View file

@ -0,0 +1,45 @@
Read [Basics of WebGL (Drawing a Cube)](Basics%20of%20WebGL%20(Drawing%20a%20Cube).md) first.
**Vertex Shaders** define vertice positions in 3D-space. That's just a function, that defines `gl_Position` value by applying different transformations to it.
## Sample code
```c
// current vertice position {x,y,z,w}
attribute vec4 aVertexPosition;
// final vertice position with all transformations applied,
// that will be passed to Fragment Shader
varying vec4 v_positionWithOffset;
// Parameters passed from Javascript loop
uniform float slide;
uniform float aspect;
void main(){
// float array of 4 elements, same as [slide,slide,slide,1]
vec4 scale=vec4(vec3(slide),1);
// float array of 4 elements, same as [aspect,1,1,1]
vec4 aspectRatioFix=vec4(aspect,vec3(1));
// vertice position, multiplied with matrices of scale and aspect ratio
gl_Position=aVertexPosition*scale*aspectRatioFix,
// vertice offset, that will be passed to fragment shader
v_positionWithOffset=gl_Position+vec4(1,1,1,1);
}
```
## Passing parameters to VertexShader
Search for `Uniforms` [at open.gl](https://open.gl/drawing) for further reading.
There're 3 ways to pass parameters.
- `attribute` are parameters, that won't change. Good for vertex buffers.
- `uniform` are meant to change over the time. Good for passing transformations.
- `varying` are parameters, that's shared between Vertex and [Fragment Shaders](Fragment%20Shaders.md).
## Applying transformations
Every vertice position is defined as `{x,y,z,w}`, where `w` is a common denominator, that's used to achieve fast coord transformations by multiplying number of square matrices with original vertice coordinates.
==We don't change vertice position buffer==, because it's slow when being run inside Javascript loop, we ==pass transformation matrices== instead and ==multiply vertice positions with transformation matrices== inside a Graphic Card's GPU, because that's what GPU made for.
Good explanation can be found here: [open.gl](https://open.gl/transformations).