Skip to content

Instantly share code, notes, and snippets.

@mathdoodle
Last active July 22, 2020 22:25
Show Gist options
  • Save mathdoodle/1bf1ccdd7abd613b12bb792f48e36d4e to your computer and use it in GitHub Desktop.
Save mathdoodle/1bf1ccdd7abd613b12bb792f48e36d4e to your computer and use it in GitHub Desktop.
Homogeneous 3D Geometric Algebra

Homogeneous 3D Geometric Algebra

Overview

This program demonstrates how to compute using the elements of the Homogeneous Model using Geometric Algebra.

In this example, the elements that we work with are points and lines. A finite point is represented by

$$ X = \alpha (e_0 + \bar{x}) $$,

where $\alpha$ is a scalar, $X$ is a vector in the 4D homogeneous space, $e_0$ is the basis vector in the homogeneous space, and $\bar{x}$ is the vector in the 3D Euclidean space. A finite point corresponds to a point in 3D space, and as such, is affected by translations.

A infinite point has no $e_0$ component and corresponds to a vector in 3D space, and as such, is invariant under translations.

A line element is represented by the outer product of two points.

The construction of these elements and the conversions between 4D and 3D space are defined in the h3ga.ts file.

The main file, index.ts, constructs two points. A line is then constructed on those points.

#version 300 es
precision highp float;
in highp vec4 vColor;
out vec4 fragColor;
void main(void) {
fragColor = vColor;
}
#version 300 es
in vec3 aPosition;
uniform vec3 uColor;
uniform float uOpacity;
uniform mat4 uModel;
uniform mat4 uProjection;
uniform mat4 uView;
out highp vec4 vColor;
void main(void) {
gl_Position = uProjection * uView * uModel * vec4(aPosition, 1.0);
vColor = vec4(uColor, uOpacity);
}
/**
* Displays an exception by writing it to a <pre> element.
*/
export function displayError(e: any) {
const stderr = <HTMLPreElement> document.getElementById('error')
stderr.style.color = "#FF0000"
stderr.innerHTML = `${e}`
}
import { VectorE3 } from 'davinci-eight'
import { algebra, NumberFieldAdapter, Multivector } from 'geocas'
/**
* A (diagonal) metric for a 4D space.
* It doesn't matter whether we use +1 or -1 for the signature corresponding to e0
*/
const G4 = algebra([1, 1, 1, 1], new NumberFieldAdapter(), ['e', 'i', 'j', 'k'])
export const one = G4.one
export const e0 = G4.unit(0)
export const e1 = G4.unit(1)
export const e2 = G4.unit(2)
export const e3 = G4.unit(3)
const e0inverse = e0.inv()
/**
* Constructs the representation of a vector in Homogeneous 3D space.
* pos is the vector in 3D space.
* Returns the vector in 4D homogeneous space.
*/
export function vector(pos: VectorE3): Multivector<number> {
return pos.x * e1 + pos.y * e2 + pos.z * e3
}
export function toVectorE3(x: Multivector<number>): VectorE3 {
return { x: x.scp(e1).scalarCoordinate(), y: x.scp(e2).scalarCoordinate(), z: x.scp(e3).scalarCoordinate() }
}
/**
* Constructs the representation of a point in Homogeneous 3D space.
* pos is the vector in 3D space.
* Returns the point in 4D homogeneous space.
*/
export function point(pos: VectorE3): Multivector<number> {
return e0 + vector(pos)
}
/**
* Translate an element of the Homogeneous model.
* X is the element of the model.
* t is the translation vector.
*/
export function translate(X: Multivector<number>, t: Multivector<number>): Multivector<number> {
return X + t ^ (e0inverse << X)
}
/**
* Computes the string representation of a Homogeneous multivector.
*/
export function repr(m: Multivector<number>): string {
return m.asString(['e0', 'e1', 'e2', 'e3'])
}
/**
* Computes the direction of a line element.
*/
export function direction(element: Multivector<number>): VectorE3 {
// TODO: This probably could be generalized to compute the direction
// of points, lines, and planes by returning an EIGHT.Geometric3.
// The return types would be scalar, vector, bivector (respectively).
const a = e0inverse << element
return toVectorE3(a)
}
/**
* The support is the point in the element (point, line, ...) that is closest to the origin.
*/
export function support(element: Multivector<number>): VectorE3 {
// Versor inverse is supported so this function should cover most cases.
const d = (e0inverse << (e0 ^ element)).div(e0inverse << element)
return toVectorE3(d)
}
<!DOCTYPE html>
<html>
<head>
<base href='/'>
<script src='https://jspm.io/[email protected]'></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id='container'>
<canvas id='canvas3D' width='500' height='500'></canvas>
<canvas id='canvas2D' width='500' height='500'></canvas>
</div>
<pre id='error'></pre>
<script>
System.defaultJSExtensions = true
System.import('./index')
</script>
</body>
</html>
import { Geometric3 } from 'davinci-eight'
import { Engine, Capability, Scene } from 'davinci-eight'
import { Facet, DirectionalLight, PerspectiveCamera } from 'davinci-eight'
import { TrackballControls } from 'davinci-eight'
import { Color } from 'davinci-eight'
import { Sphere } from 'davinci-eight'
import { Diagram3D } from 'davinci-eight'
import { windowResizer } from './windowResizer'
const zero = Geometric3.zero(true)
const e1 = Geometric3.e1(true)
const e3 = Geometric3.e3(true)
import { point, translate, vector } from './h3ga'
import { direction, support } from './h3ga'
// Comment out the following line to use the standard window.requestAnimationFrame
import { requestAnimationFrame } from './requestAnimationFrame'
import { WireCube } from './WireCube'
import { WireLine } from './WireLine'
const engine = new Engine('canvas3D')
.size(500, 500)
.clearColor(0.0, 0.0, 0.0, 1.0)
.enable(Capability.DEPTH_TEST)
const scene = new Scene(engine)
const ambients: Facet[] = []
const camera = new PerspectiveCamera()
//
// The following lines move the camera using the Homogeneous Model.
// It's a rather roundabout way of doing something quite simple, but the
// important point is that the translate function distinguishes points from vectors
// and also works for other elements in the model.
//
/**
* The position of the camera in homogeneous coordinates.
*/
const eye = translate(point(camera.eye), vector(1.5 * e3))
camera.eye.copyVector(support(eye))
ambients.push(camera)
const dirLight = new DirectionalLight()
ambients.push(dirLight)
const trackball = new TrackballControls(camera, window)
trackball.subscribe(engine.canvas)
// trackball.noPan = true
/**
* This graphic is just here to create a reference object.
*/
const wireCube = new WireCube(engine)
scene.add(wireCube)
/**
* sphere will represent points.
* We don't add it to the scene.
* Instead, we'll render manually in the animation loop.
*/
const sphere = new Sphere(engine)
sphere.radius = 0.03
/**
* wireLine will be used to visualize lines.
* We don't add it to the scene.
* Instead, we'll render manually in the animation loop.
*/
const wireLine = new WireLine(engine)
wireLine.color = Color.slateblue
const P = translate(point(+0.5 * e1), vector(zero))
const Q = translate(point(-0.5 * e1), vector(zero))
/**
* A line is the outer product of two points.
*/
const L = translate(P ^ Q, vector(zero))
const overlay = new Diagram3D('canvas2D', camera)
const animate = function() {
engine.clear()
overlay.clear()
trackball.update()
dirLight.direction.copy(camera.look).sub(camera.eye)
overlay.ctx.fillStyle = "#FFFFFF"
overlay.ctx.font = "20px sans-serif"
wireLine.a.copyVector(direction(L))
wireLine.d.copyVector(support(L))
wireLine.render(ambients)
overlay.fillText('L = P ^ Q', wireLine.d)
sphere.X.copyVector(support(P))
sphere.color = Color.red
sphere.render(ambients)
overlay.fillText('P', sphere.X)
sphere.X.copyVector(support(Q))
sphere.color = Color.cobalt
sphere.render(ambients)
overlay.fillText('Q', sphere.X)
scene.render(ambients)
requestAnimationFrame(animate)
}
windowResizer(engine, camera, overlay).resize()
requestAnimationFrame(animate)
#version 300 es
precision highp float;
in highp vec4 vColor;
out vec4 fragColor;
void main(void) {
fragColor = vColor;
}
#version 300 es
in float alpha;
uniform vec3 a;
uniform vec3 d;
uniform vec3 uColor;
uniform float uOpacity;
uniform mat4 uModel;
uniform mat4 uProjection;
uniform mat4 uView;
out highp vec4 vColor;
void main(void) {
gl_Position = uProjection * uView * uModel * vec4(alpha * a + d, 1.0);
vColor = vec4(uColor, uOpacity);
}
/**
* Universal exponential function for more natural notation.
*/
export function exp<T extends { exp: () => T }>(arg: T): T {
return arg.exp()
}
{
"description": "Homogeneous 3D Geometric Algebra",
"dependencies": {
"geocas": "2.0.0",
"davinci-eight": "8.1.0"
},
"operatorOverloading": true,
"name": "homogeneous-3d-geometric-algebra",
"version": "1.0.0",
"keywords": [
"davinci-eight",
"geocas",
"Multivector",
"Homogeneous",
"h3ga",
"mathdoodle"
],
"author": "David Geo Holmes"
}
import { displayError } from './displayError'
/**
* Catches exceptions thrown in the animation callback and displays them.
* This function will have a slight performance impact owing to the try...catch statement.
* This function may be bypassed for production use by using window.requestAnimationFrame directly.
*/
export function requestAnimationFrame(callback: FrameRequestCallback): number {
const wrapper: FrameRequestCallback = function(time: number) {
try {
callback(time)
}
catch (e) {
displayError(e)
}
}
return window.requestAnimationFrame(wrapper)
}
body {
margin: 0;
background: black;
}
#container {
position: relative;
}
#canvas3D {
position: absolute;
left: 0px;
top: 0px;
width: 500px;
height: 500px;
}
#canvas2D {
position: absolute;
left: 0px;
top: 0px;
z-index: 10;
width: 500px;
height: 500px;
/**
* Allow events to go to the other elements
*/
pointer-events: none;
}
#stats {
position: absolute;
top: 0;
left: 0;
}
{
"allowJs": false,
"declaration": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"jsx": "react",
"module": "system",
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"preserveConstEnums": true,
"removeComments": false,
"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": [
false,
"never"
],
"trailing-comma": [
true,
{
"multiline": "never",
"singleline": "never"
}
],
"triple-equals": true,
"use-isnan": true
}
}
import { Engine } from 'davinci-eight'
import { PerspectiveCamera } from 'davinci-eight'
import { Diagram3D } from 'davinci-eight'
export interface Resizer {
resize(): this
stop(): this
}
/**
* Creates an object that manages resizing of the output to fit the window.
*/
export function windowResizer(engine: Engine, camera: PerspectiveCamera, diagram: Diagram3D): Resizer {
const callback = function() {
engine.size(window.innerWidth, window.innerHeight)
engine.viewport(0, 0, window.innerWidth, window.innerHeight)
engine.canvas.width = window.innerWidth
engine.canvas.height = window.innerHeight
engine.canvas.style.width = `${window.innerWidth}px`
engine.canvas.style.height = `${window.innerHeight}px`
diagram.canvas.width = window.innerWidth
diagram.canvas.height = window.innerHeight
diagram.canvas.style.width = `${window.innerWidth}px`
diagram.canvas.style.height = `${window.innerHeight}px`
camera.aspect = window.innerWidth / window.innerHeight
}
window.addEventListener('resize', callback, false)
const that: Resizer = {
/**
*
*/
resize: function() {
callback()
return that
},
/**
* Stop watching window resize
*/
stop: function() {
window.removeEventListener('resize', callback)
return that
}
}
return that
}
import { BeginMode, DataType, Usage } from 'davinci-eight'
import { Engine, Geometry, Material, Mesh, HTMLScriptsMaterial } from 'davinci-eight'
/**
* A Geometry for rendering a cube made from lines.
* This could be done more easily using GeometryArrays.
*/
export class WireCubeGeometry implements Geometry {
private buffer: WebGLBuffer
public readonly data: Float32Array
/**
*
*/
public invalid = true
constructor(private engine: Engine) {
const gl = engine.gl
const size = 1
const L = size / 2
this.data = new Float32Array([
-L, -L, -L, +L, -L, -L,
-L, +L, -L, +L, +L, -L,
-L, -L, +L, +L, -L, +L,
-L, +L, +L, +L, +L, +L,
-L, +L, +L, -L, +L, -L,
+L, +L, +L, +L, +L, -L,
-L, -L, +L, -L, -L, -L,
+L, -L, +L, +L, -L, -L,
-L, -L, -L, -L, +L, -L,
+L, -L, -L, +L, +L, -L,
-L, -L, +L, -L, +L, +L,
+L, -L, +L, +L, +L, +L
])
this.buffer = denullify('createBuffer', gl.createBuffer())
}
bind(material: Material): void {
const gl = this.engine.gl
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer)
const aPosition = material.getAttribLocation('aPosition')
gl.vertexAttribPointer(aPosition, 3, DataType.FLOAT, true, 0, 0)
gl.enableVertexAttribArray(aPosition)
}
unbind(material: Material): void {
const gl = this.engine.gl
const aPosition = material.getAttribLocation('aPosition')
gl.disableVertexAttribArray(aPosition)
gl.bindBuffer(gl.ARRAY_BUFFER, null)
}
draw(): void {
const gl = this.engine.gl
if (this.invalid) {
gl.bufferData(gl.ARRAY_BUFFER, this.data, Usage.STATIC_DRAW)
this.invalid = false
}
gl.drawArrays(BeginMode.LINES, 0, 24)
}
setUniforms(/* visitor: FacetVisitor */): void {
// Nothing to do.
}
}
export class WireCube extends Mesh<WireCubeGeometry, Material> {
constructor(engine: Engine) {
super(new WireCubeGeometry(engine), new HTMLScriptsMaterial(engine, ['cube-vs', 'cube-fs']), engine)
}
setUniforms() {
// We don't have any special uniforms so there is nothing to set.
// However, this is how you would do it...
// const material = this.material;
// material.uniform3f('a', this.a.x, this.a.y, this.a.z);
// material.release();
super.setUniforms()
// FIXME: This is just noise. It does not add any value.
return this
}
}
/**
* Converts a null argument into an exception.
*/
function denullify<T>(name: string, arg: T | null): T {
if (arg !== null) {
return arg
}
else {
throw new Error(`${name} was null.`)
}
}
import { BeginMode, DataType, Usage } from 'davinci-eight'
import { Engine, Geometry, Material, Mesh, HTMLScriptsMaterial } from 'davinci-eight'
import { Geometric3 } from 'davinci-eight'
/**
* A Geometry for rendering a parameterized line.
*/
export class WireLineGeometry implements Geometry {
private buffer: WebGLBuffer
public data: Float32Array
/**
*
*/
public invalid = true
constructor(private engine: Engine) {
const gl = engine.gl
this.data = new Float32Array([-1, 0, 0, +1])
this.buffer = denullify('createBuffer', gl.createBuffer())
}
bind(material: Material): void {
const gl = this.engine.gl
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer)
const alpha = material.getAttribLocation('alpha')
gl.vertexAttribPointer(alpha, 1, DataType.FLOAT, true, 0, 0)
gl.enableVertexAttribArray(alpha)
}
unbind(material: Material): void {
const gl = this.engine.gl
const alpha = material.getAttribLocation('alpha')
gl.disableVertexAttribArray(alpha)
gl.bindBuffer(gl.ARRAY_BUFFER, null)
}
draw(): void {
const gl = this.engine.gl
if (this.invalid) {
gl.bufferData(gl.ARRAY_BUFFER, this.data, Usage.STATIC_DRAW)
this.invalid = false
}
gl.drawArrays(BeginMode.LINES, 0, 4)
}
setUniforms(/* visitor: EIGHT.FacetVisitor */): void {
// Nothing to do.
}
}
export class WireLine extends Mesh<WireLineGeometry, Material> {
/**
* The direction of the line.
*/
public a = Geometric3.e1().clone()
/**
* The support of the line.
*/
public d = Geometric3.zero().clone()
constructor(engine: Engine) {
super(new WireLineGeometry(engine), new HTMLScriptsMaterial(engine, ['line-vs', 'line-fs']), engine)
}
setUniforms(): WireLine {
const material = this.material
material.uniform3f('a', this.a.x, this.a.y, this.a.z)
material.uniform3f('d', this.d.x, this.d.y, this.d.z)
// FIXME: Why does release give Object is possibly 'undefined'?
if (material) {
// material.release();
}
super.setUniforms()
return this
}
}
/**
* Converts a null argument into an exception.
*/
function denullify<T>(name: string, arg: T | null): T {
if (arg !== null) {
return arg
}
else {
throw new Error(`${name} was null.`)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment