Skip to content

Instantly share code, notes, and snippets.

@mathdoodle
Last active July 7, 2020 20:19
Show Gist options
  • Save mathdoodle/823a7918fec5da1c7b075a0b37445f80 to your computer and use it in GitHub Desktop.
Save mathdoodle/823a7918fec5da1c7b075a0b37445f80 to your computer and use it in GitHub Desktop.
2D Geometry WebGL Scaffold

2D Geometry WebGL Scaffold

Overview

A WebGL scaffold for Common Core Math and Geometric Algebra in the Plane.

/**
* Displays an exception by writing it to a <pre> element.
*/
export function displayError(e: any) {
const stderr = <HTMLPreElement> document.getElementById('my-output')
stderr.style.color = "#FF0000"
stderr.innerHTML = `${e}`
}
import { Geometric2 } from 'davinci-eight'
export enum BeginMode {
POINTS = 0x0000,
LINES = 0x0001,
LINE_LOOP = 0x0002,
LINE_STRIP = 0x0003,
TRIANGLES = 0x0004,
TRIANGLE_STRIP = 0x0005,
TRIANGLE_FAN = 0x0006
}
export enum Target {
ARRAY_BUFFER = 0x8892,
ELEMENT_ARRAY_BUFFER = 0x8893,
ARRAY_BUFFER_BINDING = 0x8894,
ELEMENT_ARRAY_BUFFER_BINDING = 0x8895
}
export enum Usage {
STREAM_DRAW = 0x88E0,
STATIC_DRAW = 0x88E4,
DYNAMIC_DRAW = 0x88E8
}
/**
* A wrapper around the WebGLRenderingContext.
*/
export interface Engine {
gl: WebGLRenderingContext
}
export interface Material {
gl: WebGLRenderingContext
program: WebGLProgram
use(): void
}
export interface Geometry {
bind(): void
draw(material: Material): void
unbind(): void
}
export interface Mesh {
X: Geometric2
R: Geometric2
render(): void
}
/**
* Creates a WebGLProgram with compiled and linked shaders.
*/
function createProgram(gl: WebGLRenderingContext, vertexShader: string, fragmentShader: string): WebGLProgram {
// TODO: Proper cleanup if we throw an error at any point.
const vs = gl.createShader(gl.VERTEX_SHADER)
gl.shaderSource(vs, vertexShader)
gl.compileShader(vs)
if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
const log = gl.getShaderInfoLog(vs)
if (log) {
throw new Error(log)
}
}
const fs = gl.createShader(gl.FRAGMENT_SHADER)
if (fs) {
gl.shaderSource(fs, fragmentShader)
gl.compileShader(fs)
if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {
const log = gl.getShaderInfoLog(fs)
if (log) {
throw new Error(log)
}
}
}
else {
throw new Error()
}
const program = gl.createProgram()
if (program) {
gl.attachShader(program, vs)
gl.attachShader(program, fs)
gl.linkProgram(program)
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const log = gl.getProgramInfoLog(program)
if (log) {
throw new Error(log)
}
}
return program
}
else {
throw new Error()
}
}
export function material(gl: WebGLRenderingContext, vs: string, fs: string): Material {
const program = createProgram(gl, vs, fs)
const use = function() {
gl.useProgram(program)
}
const that: Material = {
get gl() {
return gl
},
get program() {
return program
},
use
}
return that
}
export function mesh(geometry: Geometry, material: Material): Mesh {
const X = Geometric2.vector(0, 0)
const R = Geometric2.one()
const render = function() {
material.use()
const uPosition = material.gl.getUniformLocation(material.program, 'uPosition')
material.gl.uniform2f(uPosition, X.x, X.y)
const uAttitude = material.gl.getUniformLocation(material.program, 'uAttitude')
material.gl.uniform2f(uAttitude, R.a, R.b)
geometry.bind()
geometry.draw(material)
geometry.unbind()
}
const mesh: Mesh = {
get X() {
return X
},
set X(value) {
X.copy(value)
},
get R() {
return R
},
set R(value) {
R.copy(value)
},
render
}
return Object.freeze(mesh)
}
function getWebGLContext(canvas: HTMLCanvasElement): WebGLRenderingContext {
if (!(canvas instanceof HTMLCanvasElement)) {
throw new Error("canvas must be an HTMLCanvasElement")
}
// Try to grab the standard context. If it fails, fallback to experimental.
const context = canvas.getContext('webgl') || canvas.getContext('experimental-webgl')
if (context) {
return context
}
else {
throw new Error("Unable to get WebGL context. Your browser may not support it.")
}
}
export function engine(canvasId: string): Engine {
const canvas = <HTMLCanvasElement> document.getElementById(canvasId)
if (!canvas) {
throw new Error(`Unable to get element with id '${canvasId}'`)
}
const gl = getWebGLContext(canvas)
const that: Engine = {
get gl() {
return gl
}
}
return that
}
<!doctype html>
<html>
<head>
<!-- STYLES-MARKER -->
<style>
/* STYLE-MARKER */
</style>
<!-- SYSTEM-SHIM-MARKER -->
<!-- SHADERS-MARKER -->
<!-- SCRIPTS-MARKER -->
</head>
<body>
<canvas id='my-canvas' width='500' height='500'>
Your browser does not support the HTML5 canvas element.
</canvas>
<pre id='my-output'></pre>
<script>
// CODE-MARKER
</script>
<script>
System.defaultJSExtensions = true
System.import('./script')
</script>
</body>
</html>
import { Geometric2 } from 'davinci-eight'
export const zero = Geometric2.zero()
export const e1 = Geometric2.e1()
export const e2 = Geometric2.e2()
/**
* PI is the area of the unit disk.
*/
export const PI = Math.PI
/**
* TAU is a complete turn.
*/
export const TAU = 2 * Math.PI
export function exp(x: Geometric2): Geometric2 {
if (x) {
return x.clone().exp()
}
else {
throw new Error("x must be a Geometric2")
}
}
{
"name": "geometry-2d-webgl",
"version": "1.0.0",
"description": "2D Geometry WebGL Scaffold",
"dependencies": {
"stats.js": "0.16.0",
"davinci-eight": "7.4.4"
},
"keywords": [
"WebGL",
"shaders",
"low level",
"GLSL",
"Graphics",
"functions",
"2D",
"Introduction",
"Geometric",
"Algebra",
"Geometry",
"Plane"
],
"author": "David Geo Holmes",
"operatorOverloading": true,
"hideConfigFiles": true,
"linting": true
}
import { displayError } from './displayError'
/**
* Catches exceptions thrown in the animation callback and displays them.
*/
export function requestFrame(callback: FrameRequestCallback): number {
const wrapper: FrameRequestCallback = function(time: number) {
try {
callback(time)
}
catch (e) {
displayError(e)
}
}
return window.requestAnimationFrame(wrapper)
}
import { engine, material, mesh } from './graphics'
import { requestFrame } from './requestFrame'
import { triangle } from './triangle'
import { zero, e1, e2, TAU, exp } from './math'
const eng = engine('my-canvas')
const gl = eng.gl
gl.clearColor(0.0, 0.0, 0.0, 1.0)
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)
gl.lineWidth(2)
const vs = (<HTMLElement> document.getElementById('shader-vs')).innerText
const fs = (<HTMLElement> document.getElementById('shader-fs')).innerText
const t1 = triangle(gl)
t1.a = zero
t1.b = e1
t1.c = e2
const m1 = mesh(t1, material(gl, vs, fs))
const t2 = triangle(gl)
t2.a = zero
t2.b = e1
t2.c = e2
const m2 = mesh(t2, material(gl, vs, fs))
/**
* Amount to increase θ per animation frame.
*/
const Δθ = TAU / 360
/**
* The initial rotation angle (attitude) of the second triangle.
*/
let θ = TAU / 4
const animate = function() {
gl.clear(gl.COLOR_BUFFER_BIT)
m1.render()
m2.X = 0.0 * e1 + 0.0 * e2
const B = e1 ^ e2
θ = θ + Δθ
m2.R = exp(-B * θ / 2)
m2.render()
requestFrame(animate)
}
requestFrame(animate)
/**
* Fragment Shader.
*
* The primary purpose of this GLSL function is to determine the color and alpha value
* of a pixel. This is done by assigning a vec4, interpreted as [red, green, blue, alpha],
* to the reserved gl_FragColor variable.
*/
/**
* The interpolated vertex color.
*/
varying mediump vec3 vColor;
void main(void) {
gl_FragColor = vec4(vColor.r, vColor.g, vColor.b, 1.0);
}
/**
* Vertex Shader
*
* The primary purpose of this GLSL function is to determine a vertex position
* in space from the specifies attribute and uniform parameters. This is done by assigning
* a vec4, interpreted as [x, y, z, w], to the reserved gl_Position variable.
*/
/**
* The position of the vertex relative to uPosition and before applying uAttitude.
*/
attribute vec2 aPosition;
/**
* The color of the vertex.
*/
attribute vec3 aColor;
/**
* The translation (vector) applied to all vertices.
*/
uniform vec2 uPosition;
/**
* The rotation (rotor) applied to all vertices.
*/
uniform vec2 uAttitude;
/**
* The color that will be passed to the fragment shader.
*/
varying mediump vec3 vColor;
/**
* Returns R * v * ~R.
* A counter-clockwise rotation through θ radians is R = exp(-B * θ / 2),
* where B = e1 ^ e2.
*/
highp vec2 rotate(in vec2 v, in vec2 R) {
/**
* a = +cos(θ/2)
*/
float a = R.x;
/**
* b = -sin(θ/2)
*/
float b = R.y;
/**
* c = cos(θ)
*/
float c = a * a - b * b;
/**
* s = -sin(θ)
*/
float s = 2.0 * a * b;
/**
* The simplification of the rotation equation has led to the familiar
* Linear Algebra rotation matrix,
* Q = cos(θ) -sin(θ)
* sin(θ) cos(θ) for a counter-clockwise θ rotation in the plane.
* Q operates on the column vector [x, y] (transposed) from the left.
*/
return vec2(c * v.x + s * v.y, c * v.y - s * v.x);
}
/**
* Returns v + T
*/
highp vec2 translate(in vec2 v, in vec2 T) {
// We compute the translation by components to be explicit.
// Note that WebGL permits the following form which is probably optimized too.
// return v + T;
return vec2(v.x + T.x, v.y + T.y);
}
void main(void) {
// Calculations are performed using Geometric Algebra.
// Note: It is usual in WebGL to use projective coordinates in order to unify
// translations and rotations into matrix multiplication
vec2 v = translate(rotate(aPosition, uAttitude), uPosition);
gl_Position = vec4(v.x, v.y, 0.0, 1.0);
vColor = aColor;
}
body {
background-color: #242424;
overflow: hidden;
}
canvas {
background-color: white;
position: relative;
left: 10px;
top: 10px;
z-index: 2;
}
pre {
position: relative;
color: rgb(255, 255, 255);
left: 10px;
z-index: 1;
}
import { Geometric2, Color } from 'davinci-eight'
import { BeginMode, Geometry, Material, Target, Usage } from './graphics'
const BYTES_PER_FLOAT = 4
export interface Triangle extends Geometry {
a: Geometric2
b: Geometric2
c: Geometric2
}
export function triangle(gl: WebGLRenderingContext): Triangle {
const a = Geometric2.zero()
const b = Geometric2.vector(1, 0)
const c = Geometric2.vector(0, 1)
const colorA = Color.red
const colorB = Color.green
const colorC = Color.blue
const data = new Float32Array([
a.x, a.y, colorA.r, colorA.g, colorA.b,
b.x, b.y, colorB.r, colorB.g, colorB.b,
c.x, c.y, colorC.r, colorC.g, colorC.b
])
const vbo = gl.createBuffer()
const bind = function() {
gl.bindBuffer(Target.ARRAY_BUFFER, vbo)
}
const draw = function(material: Material) {
if (typeof material !== 'object') {
throw new Error("material must be an object.")
}
// This could should be optimized.
upload()
const aPosition = gl.getAttribLocation(material.program, 'aPosition')
const aColor = gl.getAttribLocation(material.program, 'aColor')
const stride = 5 * BYTES_PER_FLOAT
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, stride, 0)
gl.vertexAttribPointer(aColor, 3, gl.FLOAT, false, stride, 2 * BYTES_PER_FLOAT)
enableAttribs(material)
gl.drawArrays(BeginMode.LINE_LOOP, 0, 3)
disableAttribs(material)
}
const unbind = function() {
gl.bindBuffer(Target.ARRAY_BUFFER, null)
}
const enableAttribs = function(material: Material) {
const aPosition = gl.getAttribLocation(material.program, 'aPosition')
gl.enableVertexAttribArray(aPosition)
const aColor = gl.getAttribLocation(material.program, 'aColor')
gl.enableVertexAttribArray(aColor)
}
const disableAttribs = function(material: Material) {
const aPosition = gl.getAttribLocation(material.program, 'aPosition')
gl.disableVertexAttribArray(aPosition)
const aColor = gl.getAttribLocation(material.program, 'aColor')
gl.disableVertexAttribArray(aColor)
}
const upload = function() {
data[0] = a.x
data[1] = a.y
data[2] = colorA.r
data[3] = colorA.g
data[4] = colorA.b
data[5] = b.x
data[6] = b.y
data[7] = colorB.r
data[8] = colorB.g
data[9] = colorB.b
data[10] = c.x
data[11] = c.y
data[12] = colorC.r
data[13] = colorC.g
data[14] = colorC.b
gl.bufferData(Target.ARRAY_BUFFER, data, Usage.DYNAMIC_DRAW)
}
const geo: Triangle = {
get a() {
return a
},
set a(value) {
a.copyVector(value)
},
get b() {
return b
},
set b(value) {
b.copyVector(value)
},
get c() {
return c
},
set c(value) {
c.copyVector(value)
},
bind,
draw,
unbind
}
return Object.freeze(geo)
}
{
"allowJs": true,
"checkJs": true,
"declaration": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"jsx": "react",
"module": "system",
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"preserveConstEnums": true,
"removeComments": false,
"skipLibCheck": true,
"sourceMap": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"target": "es5",
"traceResolution": true
}
{
"rules": {
"array-type": [
true,
"array"
],
"curly": false,
"comment-format": [
true,
"check-space"
],
"eofline": true,
"forin": true,
"jsdoc-format": true,
"no-conditional-assignment": false,
"no-consecutive-blank-lines": true,
"no-construct": true,
"no-for-in-array": true,
"no-magic-numbers": false,
"no-shadowed-variable": true,
"no-string-throw": true,
"no-trailing-whitespace": [
true,
"ignore-jsdoc"
],
"no-var-keyword": true,
"one-variable-per-declaration": [
true,
"ignore-for-loop"
],
"prefer-const": true,
"prefer-for-of": true,
"prefer-function-over-method": false,
"radix": true,
"semicolon": [
true,
"never"
],
"trailing-comma": [
true,
{
"multiline": "never",
"singleline": "never"
}
],
"triple-equals": true,
"use-isnan": true
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment