From 03ddf6011be422296b5bea50d663d22c42b259d5 Mon Sep 17 00:00:00 2001 From: Misode Date: Thu, 9 Feb 2023 18:03:15 +0100 Subject: [PATCH] Initial working version of transformation preview - missing ability to modify matrix - sdvDecompose is still probably wrong - missing ability to edit rotations using axis + angle --- src/app/App.tsx | 3 +- src/app/Utils.ts | 178 ++++++++++++++++++- src/app/components/Octicon.tsx | 2 + src/app/pages/Home.tsx | 3 + src/app/pages/Transformation.tsx | 288 +++++++++++++++++++++++++++++++ src/app/pages/index.ts | 1 + src/locales/en.json | 7 + src/styles/global.css | 57 ++++++ 8 files changed, 536 insertions(+), 3 deletions(-) create mode 100644 src/app/pages/Transformation.tsx diff --git a/src/app/App.tsx b/src/app/App.tsx index dcfb2a69..0a359ff6 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -4,7 +4,7 @@ import '../styles/global.css' import '../styles/nodes.css' import { Analytics } from './Analytics.js' import { Header } from './components/index.js' -import { Changelog, Generator, Generators, Guide, Guides, Home, Partners, Sounds, Versions, Worldgen } from './pages/index.js' +import { Changelog, Generator, Generators, Guide, Guides, Home, Partners, Sounds, Transformation, Versions, Worldgen } from './pages/index.js' import { cleanUrl } from './Utils.js' export function App() { @@ -24,6 +24,7 @@ export function App() { + diff --git a/src/app/Utils.ts b/src/app/Utils.ts index 17f60bdb..055a3aad 100644 --- a/src/app/Utils.ts +++ b/src/app/Utils.ts @@ -2,8 +2,7 @@ import type { DataModel } from '@mcschema/core' import { Path } from '@mcschema/core' import * as zip from '@zip.js/zip.js' import type { Random } from 'deepslate/core' -import type { mat3 } from 'gl-matrix' -import { vec2 } from 'gl-matrix' +import { mat3, mat4, quat, vec2, vec3 } from 'gl-matrix' import yaml from 'js-yaml' import { route } from 'preact-router' import rfdc from 'rfdc' @@ -377,3 +376,178 @@ export function iterateWorld2D(img: ImageData, transform: mat3, getData: (x: img.data[4 * i + 3] = 255 } } + +function makeFloat(a: number) { + return a > 3.4028235E38 ? Infinity : a < -3.4028235E38 ? -Infinity : a +} + +const G = 3 + 2 * Math.sqrt(2) +const CS = Math.cos(Math.PI / 8) +const SS = Math.sin(Math.PI / 8) +function approxGivensQuat(a: number, b: number, c: number): [number, number] { + const d = 2 * (a - c) + if (makeFloat(G * b * b) < makeFloat(d * d)) { + const e = 1 / Math.sqrt(b * b + d * d) + return [e * b, e * d] + } else { + return [SS, CS] + } +} + +function qrGivensQuat(a: number, b: number) { + const c = Math.hypot(a, b) + let d = c > 1e-6 ? b : 0 + let e = Math.abs(a) + Math.max(c, 1e-6) + if (a < 0) { + [d, e] = [e, d] + } + const f = 1 / Math.sqrt(e * e + d * d) + return [d * f, e * f] +} + +// modifies the passed mat3 +function stepJacobi(m: mat3): quat { + const n = mat3.create() + const q = quat.create() + if (m[1] * m[1] + m[3] * m[3] > 1e-6) { + const [a, b] = approxGivensQuat(m[0], 0.5 * (m[1] + m[3]), m[4]) + const r = quat.fromValues(0, 0, a, b) + const c = b * b - a * a + const d = -2 * a * b + const e = b * b + a * a + quat.mul(q, q, r) + n[0] = c + n[4] = c + n[1] = -d + n[3] = d + n[8] = e + mat3.mul(m, m, n) + mat3.transpose(n, n) + mat3.mul(n, n, m) + mat3.copy(m, n) + } + // console.log('J1', q, m) + if (m[2] * m[2] + m[6] * m[6] > 1e-6) { + const pair = approxGivensQuat(m[0], 0.5 * (m[2] + m[6]), m[8]) + const a = -pair[0] + const b = pair[1] + const r = quat.fromValues(0, a, 0, b) + const c = b * b - a * a + const d = -2 * a * b + const e = b * b + a * a + quat.mul(q, q, r) + n[0] = c + n[8] = c + n[2] = d + n[6] = -d + n[4] = e + mat3.mul(m, m, n) + mat3.transpose(n, n) + mat3.mul(n, n, m) + mat3.copy(m, n) + } + // console.log('J2', q, m) + if (m[5] * m[5] + m[7] * m[7] > 1e-6) { + const [a, b] = approxGivensQuat(m[4], 0.5 * (m[5] + m[7]), m[8]) + const r = quat.fromValues(a, 0, 0, b) + const c = b * b - a * a + const d = -2 * a * b + const e = b * b + a * a + quat.mul(q, q, r) + n[4] = c + n[8] = c + n[5] = -d + n[7] = d + n[0] = e + mat3.mul(m, m, n) + mat3.transpose(n, n) + mat3.mul(n, n, m) + mat3.copy(m, n) + } + // console.log('J3', q, m) + return q +} + +export function svdDecompose(m: mat3): [quat, vec3, quat] { + const q = quat.create() + const r = quat.create() + const n = mat3.create() + mat3.transpose(n, m) + mat3.mul(n, n, m) + // console.log('A', n) + + for (let i = 0; i < 5; i += 1) { + quat.mul(r, r, stepJacobi(n)) + } + quat.normalize(r, r) + // console.log('B', r) + const p0 = mat3.create() + mat3.fromQuat(p0, r) + mat3.mul(p0, m, p0) + // console.log('C', p0) + let f = 1 + + const [a1, b1] = qrGivensQuat(p0[0], p0[1]) + const c1 = b1 * b1 - a1 * a1 + const d1 = -2 * a1 * b1 + const e1 = b1 * b1 + a1 * a1 + const s1 = quat.fromValues(0, 0, a1, b1) + // console.log('D', s1) + quat.mul(q, q, s1) + const p1 = mat3.create() + p1[0] = c1 + p1[4] = c1 + p1[1] = d1 + p1[3] = -d1 + p1[8] = e1 + f *= e1 + mat3.mul(p1, p1, p0) + // console.log('E', p1) + + const pair = qrGivensQuat(p1[0], p1[2]) + const a2 = -pair[0] + const b2 = pair[1] + const c2 = b2 * b2 - a2 * a2 + const d2 = -2 * a2 * b2 + const e2 = b2 * b2 + a2 * a2 + const s2 = quat.fromValues(0, a2, 0, b2) + quat.mul(q, q, s2) + const p2 = mat3.create() + p2[0] = c2 + p2[8] = c2 + p2[2] = -d2 + p2[6] = d2 + p2[4] = e2 + f *= e2 + // console.log('H2', f, e2) + mat3.mul(p2, p2, p1) + + const [a3, b3] = qrGivensQuat(p2[4], p2[5]) + const c3 = b3 * b3 - a3 * a3 + const d3 = -2 * a3 * b3 + const e3 = b3 * b3 + a3 * a3 + const s3 = quat.fromValues(a3, 0, 0, b3) + quat.mul(q, q, s3) + const p3 = mat3.create() + p3[4] = c3 + p3[8] = c3 + p3[5] = d3 + p3[7] = -d3 + p3[0] = e3 + f *= e3 + mat3.mul(p3, p3, p2) + // console.log('G', p1) + + f = 1 / f + quat.scale(q, q, Math.sqrt(f)) + const scale = vec3.fromValues(p3[0] * f, p3[4] * f, p3[8] * f) + return [q, scale, r] +} + +export function toAffine(m: mat4): mat4 { + if (m[15] === 0) m[15] = 1 + const a = 1 / m[15] + const n = mat4.clone(m) + mat4.scale(n, n, [a, a, a]) + return n +} diff --git a/src/app/components/Octicon.tsx b/src/app/components/Octicon.tsx index 9c047fef..16cd01b2 100644 --- a/src/app/components/Octicon.tsx +++ b/src/app/components/Octicon.tsx @@ -32,6 +32,7 @@ export const Octicon = { kebab_horizontal: , link: , link_external: , + lock: , mark_github: , moon: , package: , @@ -55,6 +56,7 @@ export const Octicon = { three_bars: , trashcan: , unfold: , + unlock: , upload: , x: , x_circle: , diff --git a/src/app/pages/Home.tsx b/src/app/pages/Home.tsx index 92079de9..de54a002 100644 --- a/src/app/pages/Home.tsx +++ b/src/app/pages/Home.tsx @@ -92,6 +92,9 @@ function Tools() { + diff --git a/src/app/pages/Transformation.tsx b/src/app/pages/Transformation.tsx new file mode 100644 index 00000000..56f93c65 --- /dev/null +++ b/src/app/pages/Transformation.tsx @@ -0,0 +1,288 @@ +import type { Color } from 'deepslate' +import { Mesh, Quad, Renderer, ShaderProgram, Vector } from 'deepslate' +import { mat3, mat4, quat, vec3 } from 'gl-matrix' +import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks' +import { Footer, NumberInput, Octicon, RangeInput } from '../components/index.js' +import { InteractiveCanvas3D } from '../components/previews/InteractiveCanvas3D.jsx' +import { useLocale, useTitle } from '../contexts/index.js' +import { svdDecompose, toAffine } from '../Utils.js' + +interface Props { + path?: string, +} +export function Transformation({}: Props) { + const { locale } = useLocale() + useTitle(locale('title.transformation')) + + const [translation, setTranslation] = useState(vec3.create()) + const [leftRotation, setLeftRotation] = useState(quat.create()) + const [scale, setScale] = useState(vec3.fromValues(1, 1, 1)) + const [rightRotation, setRightRotation] = useState(quat.create()) + + const [normalizeLeft, setNormalizeLeft] = useState(true) + const [normalizeRight, setNormalizeRight] = useState(true) + + useEffect(() => { + if (normalizeLeft) setLeftRotation(q => quat.normalize(quat.clone(q), q)) + }, [normalizeLeft]) + useEffect(() => { + if (normalizeRight) setRightRotation(q => quat.normalize(quat.clone(q), q)) + }, [normalizeRight]) + + const matrix = useMemo(() => { + const m = mat4.create() + mat4.translate(m, m, translation) + mat4.mul(m, m, mat4.fromQuat(mat4.create(), leftRotation)) + mat4.scale(m, m, scale) + mat4.mul(m, m, mat4.fromQuat(mat4.create(), rightRotation)) + return m + }, [translation, leftRotation, scale, rightRotation]) + + const setMatrix = useCallback((m: mat4) => { + const affine = toAffine(m) + const newTranslation = mat4.getTranslation(vec3.create(), affine) + const [newLeftRotation, newScale, newRightRotation] = svdDecompose(mat3.fromMat4(mat3.create(), affine)) + setTranslation(newTranslation) + setLeftRotation(newLeftRotation) + setScale(newScale) + setRightRotation(newRightRotation) + }, []) + + const changeMatrix = useCallback((i: number, value: number) => { + const m = mat4.clone(matrix) + m[i] = value + setMatrix(m) + }, [matrix]) + + const changeTranslation = useCallback((i: number, value: number) => { + const copy = vec3.clone(translation) + copy[i] = value + setTranslation(copy) + }, [translation]) + + const changeLeftRotation = useCallback((i: number, value: number) => { + const copy = quat.clone(leftRotation) + copy[i] = value + if (normalizeLeft) { + quat.normalize(copy, copy) + } + setLeftRotation(copy) + }, [leftRotation, normalizeLeft]) + + const changeScale = useCallback((i: number, value: number) => { + const copy = vec3.clone(scale) + copy[i] = value + setScale(copy) + }, [scale]) + + const changeRightRotation = useCallback((i: number, value: number) => { + const copy = quat.clone(rightRotation) + copy[i] = value + if (normalizeRight) { + quat.normalize(copy, copy) + } + setRightRotation(copy) + }, [rightRotation, normalizeRight]) + + const renderer = useRef() + const onSetup = useCallback((canvas: HTMLCanvasElement) => { + const gl = canvas.getContext('webgl') + if (!gl) return + renderer.current = new MeshRenderer(gl) + }, []) + const onResize = useCallback((width: number, height: number) => { + renderer.current?.setViewport(0, 0, width, height) + }, []) + const onDraw = useCallback((view: mat4) => { + renderer.current?.draw(view, matrix) + }, [matrix]) + + return
+
+
+
+
+ {locale('transformation.translation')} + +
+ {Array(3).fill(0).map((_, i) =>
+ changeTranslation(i, v)} /> + changeTranslation(i, v)} /> +
)} +
+
+
+ {locale('transformation.left_rotation')} + + +
+ {Array(4).fill(0).map((_, i) =>
+ changeLeftRotation(i, v)} /> + changeLeftRotation(i, v)} /> +
)} +
+
+
+ {locale('transformation.scale')} + +
+ {Array(3).fill(0).map((_, i) =>
+ changeScale(i, v)} /> + changeScale(i, v)} /> +
)} +
+
+
+ {locale('transformation.right_rotation')} + + +
+ {Array(4).fill(0).map((_, i) =>
+ changeRightRotation(i, v)} /> + changeRightRotation(i, v)} /> +
)} +
+
+
+
+
+ {locale('transformation.matrix')} + +
+ {Array(16).fill(0).map((_, i) =>
+ changeMatrix(i, v)} readonly disabled /> + changeMatrix(i, v)} readonly disabled /> +
)} +
+
+
+ +
+} + +const vsMesh = ` + attribute vec4 vertPos; + attribute vec3 vertColor; + attribute vec3 normal; + + uniform mat4 mView; + uniform mat4 mProj; + + varying highp vec3 vColor; + varying highp float vLighting; + + void main(void) { + gl_Position = mProj * mView * vertPos; + vColor = vertColor; + vLighting = normal.y * 0.2 + abs(normal.z) * 0.1 + 0.8; + } +` + +const fsMesh = ` + precision highp float; + varying highp vec3 vColor; + varying highp float vLighting; + + void main(void) { + gl_FragColor = vec4(vColor.xyz * vLighting, 1.0); + } +` + +const vsGrid = ` + attribute vec4 vertPos; + attribute vec3 vertColor; + + uniform mat4 mView; + uniform mat4 mProj; + + varying highp vec3 vColor; + + void main(void) { + gl_Position = mProj * mView * vertPos; + vColor = vertColor; + } +` + +const fsGrid = ` + precision highp float; + varying highp vec3 vColor; + + void main(void) { + gl_FragColor = vec4(vColor, 1.0); + } +` + +class MeshRenderer extends Renderer { + private readonly meshShaderProgram: WebGLProgram + private readonly gridShaderProgram: WebGLProgram + private readonly mesh: Mesh + private readonly grid: Mesh + + constructor(gl: WebGLRenderingContext) { + super(gl) + this.meshShaderProgram = new ShaderProgram(gl, vsMesh, fsMesh).getProgram() + this.gridShaderProgram = new ShaderProgram(gl, vsGrid, fsGrid).getProgram() + + const color: Color = [0.8, 0.8, 0.8] + this.mesh = new Mesh([ + Quad.fromPoints( + new Vector(1, 0, 0), + new Vector(1, 1, 0), + new Vector(1, 1, 1), + new Vector(1, 0, 1)).setColor(color), + Quad.fromPoints( + new Vector(0, 0, 1), + new Vector(0, 1, 1), + new Vector(0, 1, 0), + new Vector(0, 0, 0)).setColor(color), + Quad.fromPoints( + new Vector(0, 1, 1), + new Vector(1, 1, 1), + new Vector(1, 1, 0), + new Vector(0, 1, 0)).setColor(color), + Quad.fromPoints( + new Vector(0, 0, 0), + new Vector(1, 0, 0), + new Vector(1, 0, 1), + new Vector(0, 0, 1)).setColor(color), + Quad.fromPoints( + new Vector(0, 0, 1), + new Vector(1, 0, 1), + new Vector(1, 1, 1), + new Vector(0, 1, 1)).setColor(color), + Quad.fromPoints( + new Vector(0, 1, 0), + new Vector(1, 1, 0), + new Vector(1, 0, 0), + new Vector(0, 0, 0)).setColor(color), + ]) + for (const q of this.mesh.quads) { + const normal = q.normal() + q.forEach(v => v.normal = normal) + } + this.mesh.rebuild(this.gl, { pos: true, color: true, normal: true }) + + this.grid = new Mesh() + this.grid.addLine(0, 0, 0, 1, 0, 0, [1, 0, 0]) + this.grid.addLine(0, 0, 0, 0, 1, 0, [0, 1, 0]) + this.grid.addLine(0, 0, 0, 0, 0, 1, [0, 0, 1]) + this.grid.rebuild(this.gl, { pos: true, color: true }) + } + + public draw(view: mat4, transform: mat4) { + this.setShader(this.gridShaderProgram) + this.prepareDraw(view) + this.drawMesh(this.grid, { pos: true, color: true }) + + const copy = mat4.clone(view) + mat4.multiply(copy, copy, transform) + this.setShader(this.meshShaderProgram) + this.prepareDraw(copy) + this.drawMesh(this.mesh, { pos: true, color: true, normal: true }) + } +} diff --git a/src/app/pages/index.ts b/src/app/pages/index.ts index 5ba640f2..9017ab18 100644 --- a/src/app/pages/index.ts +++ b/src/app/pages/index.ts @@ -6,5 +6,6 @@ export * from './Guides.js' export * from './Home.js' export * from './Partners.js' export * from './Sounds.js' +export * from './Transformation.jsx' export * from './Versions.js' export * from './Worldgen.jsx' diff --git a/src/locales/en.json b/src/locales/en.json index 7307b1d1..da60dbfe 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -87,6 +87,7 @@ "more": "More", "move_down": "Move down", "move_up": "Move up", + "normalize": "Normalize", "not_found.description": "The page you were looking for does not exist.", "no_file_chosen": "No file chosen", "no_presets": "No presets", @@ -123,9 +124,15 @@ "title.project": "%0% Project", "title.new_project": "Create a new project", "title.sounds": "Sound Explorer", + "title.transformation": "Transformation Visualizer", "title.versions": "Versions Explorer", "title.worldgen": "Worldgen Generators and Guides", "tools": "Tools", + "transformation.matrix": "Matrix", + "transformation.translation": "Translation", + "transformation.left_rotation": "Left rotation", + "transformation.scale": "Scale", + "transformation.right_rotation": "Right rotation", "trim_material": "Trim material", "trim_pattern": "Trim pattern", "pack_mcmeta": "Pack.mcmeta", diff --git a/src/styles/global.css b/src/styles/global.css index a4ec179c..ce9776e5 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -571,6 +571,63 @@ main.has-project { position: absolute; } +.transformation-editor { + display: flex; + padding: 8px 16px; + flex-wrap: wrap; + gap: 16px; +} + +.transformation-input { + display: flex; + align-items: center; + margin-bottom: 2px; +} + +.transformation-input input[type=number] { + width: 100px; + padding: 3px 6px; + border: none; + border-radius: 3px; + font-size: 14px; + margin-right: 8px; + background-color: var(--background-2); + color: var(--text-2); +} + +.transformation-title { + display: flex; + margin-bottom: 2px; +} + +.transformation-title span { + margin-right: 4px; +} + +.transformation-title button { + display: flex; + justify-content: center; + align-items: center; + padding: 2px 3px; + background-color: var(--background-4); + border: none; + border-radius: 3px; + margin-left: 4px; + cursor: pointer; +} + +.transformation-title button:hover { + background-color: var(--background-5); +} + +.transformation-section:not(:first-child) { + margin-top: 8px; +} + +.transformation-matrix .transformation-input:nth-child(4n+2):not(:nth-of-type(1)) { + margin-top: 8px; +} + .btn { display: flex; align-items: center;