Skip to content

Instantly share code, notes, and snippets.

@mithi
Created August 18, 2020 09:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mithi/86f619728b590c2c9bc7d9ecfd5f046b to your computer and use it in GitHub Desktop.
Save mithi/86f619728b590c2c9bc7d9ecfd5f046b to your computer and use it in GitHub Desktop.
const radians = thetaDegrees => (thetaDegrees * Math.PI) / 180
const getSinCos = theta => [Math.sin(radians(theta)), Math.cos(radians(theta))]
const dot = (a, b) => a.x * b.x + a.y * b.y + a.z * b.z
const vectorLength = v => Math.sqrt(dot(v, v))
const vectorFromTo = (a, b) => new Vector(b.x - a.x, b.y - a.y, b.z - a.z)
const scaleVector = (v, d) => new Vector(d * v.x, d * v.y, d * v.z)
const cross = (a, b) => {
const x = a.y * b.z - a.z * b.y
const y = a.z * b.x - a.x * b.z
const z = a.x * b.y - a.y * b.x
return new Vector(x, y, z)
}
const getNormalofThreePoints = (a, b, c) => {
const ba = vectorFromTo(b, a)
const bc = vectorFromTo(b, c)
const n = cross(ba, bc)
const len_n = vectorLength(n)
const unit_n = scaleVector(n, 1 / len_n)
return unit_n
}
const uniformMatrix4x4 = d => {
const dRow = [d, d, d, d]
return [dRow.slice(), dRow.slice(), dRow.slice(), dRow.slice()]
}
const multiply4x4 = (matrixA, matrixB) => {
let resultMatrix = uniformMatrix4x4(null)
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
resultMatrix[i][j] =
matrixA[i][0] * matrixB[0][j] +
matrixA[i][1] * matrixB[1][j] +
matrixA[i][2] * matrixB[2][j] +
matrixA[i][3] * matrixB[3][j]
}
}
return resultMatrix
}
function rotX(theta, tx = 0, ty = 0, tz = 0) {
const [s, c] = getSinCos(theta)
return [
[1, 0, 0, tx],
[0, c, -s, ty],
[0, s, c, tz],
[0, 0, 0, 1],
]
}
function rotY(theta) {
const [s, c] = getSinCos(theta)
return [
[c, 0, s, 0],
[0, 1, 0, 0],
[-s, 0, c, 0],
[0, 0, 0, 1],
]
}
function rotZ(theta) {
const [s, c] = getSinCos(theta)
return [
[c, -s, 0, 0],
[s, c, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1],
]
}
const rotXYZ = eulerVec => {
const rx = rotX(eulerVec.x)
const ry = rotY(eulerVec.y)
const rz = rotZ(eulerVec.z)
const rxy = multiply4x4(rx, ry)
const rxyz = multiply4x4(rxy, rz)
return rxyz
}
class Vector {
constructor(x, y, z, name) {
this.x = x
this.y = y
this.z = z
this.name = name
}
getTransformedPoint(transformMatrix) {
const [r0, r1, r2] = transformMatrix.slice(0, 3)
const [r00, r01, r02, tx] = r0
const [r10, r11, r12, ty] = r1
const [r20, r21, r22, tz] = r2
const newX = this.x * r00 + this.y * r01 + this.z * r02 + tx
const newY = this.x * r10 + this.y * r11 + this.z * r12 + ty
const newZ = this.x * r20 + this.y * r21 + this.z * r22 + tz
return new Vector(newX, newY, newZ, this.name)
}
}
const tMatrix = translation => [
[1, 0, 0, translation.x],
[0, 1, 0, translation.y],
[0, 0, 1, translation.z],
[0, 0, 0, 1],
]
const sMatrix = s => [
[s.x, 0, 0, 0],
[0, s.y, 0, 0],
[0, 0, s.z, 0],
[0, 0, 0, 1],
]
/*
E4------F5 y
|`. | `. |
| `A0-----B1 *----- x
| | | | \
G6--|--H7 | \
`. | `. | z
`C2-----D3
*/
class NormalUnitCube {
CENTER = new Vector(0, 0, 0, "cube-center") // cube-center
POINTS = [
new Vector(-1, +1, +1, "front-top-left"), // A0
new Vector(+1, +1, +1, "front-top-right"), // B1
new Vector(-1, -1, +1, "front-bottom-left"), // C2
new Vector(+1, -1, +1, "front-bottom-right"), // D3
new Vector(-1, +1, -1, "back-top-left"), // E4
new Vector(+1, +1, -1, "back-top-right"), // F5
new Vector(-1, -1, -1, "back-bottom-left"), // G6
new Vector(+1, -1, -1, "back-bottom-right"), // H7
]
}
class Cube {
UNIT_CUBE = new NormalUnitCube()
constructor(
eulerVec = new Vector(0, 0, 0),
scale = new Vector(1, 1, 1),
translateVec = new Vector(0, 0, 0)
) {
const rMatrix = rotXYZ(eulerVec)
const s = scale
const t = translateVec
this.wrtWorldMatrix = multiply4x4(tMatrix(t), multiply4x4(sMatrix(s), rMatrix))
this.points = this.UNIT_CUBE.POINTS
}
}
const getWorldWrtCameraMatrix = (
translateVec = Vector(0, 0, 0),
eulerVec = Vector(0, 0, 0)
) => {
const r = rotXYZ(eulerVec)
const t = translateVec
// Inverse of rotations matrix
// inverse_matrix = rotateCameraMatrixInverse * translateCameraMatrixInverse
// world_to_camera_matrix
return [
[r[0][0], r[1][0], r[2][0], -t.x],
[r[0][1], r[1][1], r[2][1], -t.y],
[r[0][2], r[1][2], r[2][2], -t.z],
[0, 0, 0, 1],
]
}
const getProjectedPoint = (point, projectionConstant) => {
return new Vector(
(point.x / point.z) * projectionConstant,
(point.y / point.z) * projectionConstant,
projectionConstant,
point.name
)
}
const renderCube = (cube, cubeWrtCameraMatrix, projectionConstant) => {
let projectedPoints = []
let pointsWrtCamera = []
cube.points.forEach(point => {
const pointWrtCamera = point.getTransformedPoint(cubeWrtCameraMatrix)
const projectedPoint = getProjectedPoint(pointWrtCamera, projectionConstant)
pointsWrtCamera.push(pointWrtCamera)
projectedPoints.push(projectedPoint)
})
return [pointsWrtCamera, projectedPoints]
}
// RENDER SCENE
const renderScene = (box, cam) => {
const Z_TRANSLATE_OFFSET = 5
const PROJECTION_CONSTANT = 300 * cam.zoom
const CAMERA_POSITION = new Vector(cam.tx, cam.ty, cam.tz + Z_TRANSLATE_OFFSET)
const CAMERA_ORIENTATION = new Vector(cam.rx, cam.ry, cam.rz)
const worldWrtCameraMatrix = getWorldWrtCameraMatrix(
CAMERA_POSITION,
CAMERA_ORIENTATION
)
// euler orientation rotation
const r = new Vector(box.rx, box.ry, box.rz)
// translate vector
const t = new Vector(box.tx, box.ty, box.tz)
// scale magnitude
const s = new Vector(box.sx, box.sy, box.sz)
const cube = new Cube(r, s, t)
const cubeWrtCameraMatrix = multiply4x4(worldWrtCameraMatrix, cube.wrtWorldMatrix)
const [pointsWrtCamera, projectedPoints] = renderCube(
cube,
cubeWrtCameraMatrix,
PROJECTION_CONSTANT
)
const container = {
color: "#333333",
opacity: 1.0,
xRange: 600,
yRange: 600,
}
const isFrontFacing = whichPlanesFrontFacing(pointsWrtCamera)
const cubeData = drawBox(projectedPoints, isFrontFacing)
const planesAndLinesData = renderPlanesAndLines(
worldWrtCameraMatrix,
400,
PROJECTION_CONSTANT
)
return { container, data: [...planesAndLinesData, ...cubeData] }
}
/* * L5
(z)
* p10 L6
| P11
L4 | |
p9 | |
| | |
| * ------|---------> (y)-- L0 (x=-R/2)
| / p0 p1 p2
| / / /
| / / /
/---------/--------/-------- L1 (x=0)
/ p3 / p4 /p5
/ / /
/ p6 / /
(x)--------/-------------------- (x=R/2)
/ p7 p8
/ / /
L2 L3
(y=-R/2) (y=0) (y=R/2)
polygon: p0, p2, p8, p6
R = axisRange
* */
const renderPlanesAndLines = (worldWrtCameraMatrix, axisRange, projectionConstant) => {
const r = axisRange / 2
// prettier-ignore
const points = [
new Vector(-r, -r, 0),
new Vector(-r, 0, 0),
new Vector(-r, r, 0),
new Vector( 0, -r, 0),
new Vector( 0, 0, 0),
new Vector( 0, r, 0),
new Vector( r, -r, 0),
new Vector( r, 0, 0),
new Vector( r, r, 0),
new Vector( 0, -r, axisRange),
new Vector(-r, -r, axisRange),
new Vector(-r, 0, axisRange),
]
const p = points.map(point => {
const pointWrtCamera = point.getTransformedPoint(worldWrtCameraMatrix)
return pointWrtCamera
//return getProjectedPoint(pointWrtCamera, projectionConstant)
})
const lines = [
[p[0], p[2]],
[p[3], p[5]],
[p[0], p[6]],
[p[1], p[7]],
[p[3], p[9]],
[p[0], p[10]],
[p[1], p[11]],
]
const polygon = [p[0], p[2], p[8], p[6]]
const linesData = {
x0: lines.map(line => line[0].x),
y0: lines.map(line => line[0].y),
x1: lines.map(line => line[1].x),
y1: lines.map(line => line[1].y),
color: "#FFFFFF",
opacity: 0.9,
size: 1,
type: "lines",
id: `axis-lines`,
}
const polygonData = {
x: polygon.map(point => point.x),
y: polygon.map(point => point.y),
fillColor: "#4cd137",
fillOpacity: 0.5,
borderColor: 0,
borderOpacity: 1.0,
borderSize: 0,
type: "polygon",
id: "xy-plane",
}
return [polygonData, linesData]
}
/*
E4------F5 y
|`. | `. |
| `A0-----B1 *----- x
| | | | \
G6--|--H7 | \
`. | `. | z
`C2-----D3
face 1 - A0, B1, D3 | C2 (front)
face 2 - B1, F5, H7 | D3 (front right)
face 3 - F5, E4, G6 | H7 (front left)
face 4 - E4, A0, C2 | G6 (back)
face 5 - E4, F5, B1 | A0 (top)
face 6 - C2, D3, H7 | G6 |(bottom)
IMPORTANT!
The second point (ie B1 of set [A0, B1, D3, C2]
is the center of A0, B1, D3 which is where we will
compute the normal of the plane
*/
// use back face culling to figure out which
// faces are in front
const POINT_FACE_SET = [
[0, 1, 3, 2],
[1, 5, 7, 3],
[5, 4, 6, 7],
[4, 0, 2, 6],
[4, 5, 1, 0],
[2, 3, 7, 6],
]
// returns an array of booleans with six elements
// returns if the respective planes defined by the for each set of points (POINT_FACE_SET)
// are front facing or not
const whichPlanesFrontFacing = pointsWrtCamera => {
const p = pointsWrtCamera
return POINT_FACE_SET.map(pointIds => {
const [a, b, c] = pointIds
const n = getNormalofThreePoints(p[a], p[b], p[c])
// v is vector from point p[b]
// to cameraOriginPoint Vector(0, 0, 0)
const v = new Vector(-p[b].x, -p[b].y, -p[b].z)
const isFrontFacing = dot(n, v) > 0.0
return isFrontFacing
})
}
const drawBox = (projectedPoints, isFrontFacing) => {
const p = projectedPoints
const COLORS = ["#32ff7e", "#e056fd", "#E91E63", "#fa8231", "#fff200", "#ff3838"]
const OPACITY = [0.75, 0.75, 0.75, 0.75, 0.75, 0.75]
let data = []
isFrontFacing.forEach((isFront, index) => {
const [a, b, c, d] = POINT_FACE_SET[index]
const plane = {
x: [p[a].x, p[b].x, p[c].x, p[d].x],
y: [p[a].y, p[b].y, p[c].y, p[d].y],
borderColor: "#0652DD",
borderOpacity: 1.0,
fillColor: COLORS[index],
fillOpacity: OPACITY[index],
borderSize: 8,
type: "polygon",
id: `plane-${index}`,
}
const points = {
x: [p[a].x, p[b].x, p[c].x, p[d].x],
y: [p[a].y, p[b].y, p[c].y, p[d].y],
color: "#0652DD",
opacity: 1.0,
size: 15,
type: "points",
id: `points-${index}`,
}
data = isFront ? [...data, plane, points] : [plane, points, ...data]
})
return data
}
export { Cube, renderScene, drawBox }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment