diff --git a/src/core/math/index.ts b/src/core/math/index.ts index d4787768..32cfc163 100644 --- a/src/core/math/index.ts +++ b/src/core/math/index.ts @@ -25,14 +25,26 @@ export function floor_mod(dividend: number, divisor: number): number { return ((dividend % divisor) + divisor) % divisor; } -export class Matrix4 { - static of(...values: readonly number[]): Matrix4 { - return new Matrix4(new Float32Array(values)); +export class Vec2 { + constructor(public x: number, public y: number) {} +} + +export function vec2_diff(v: Vec2, w: Vec2): Vec2 { + return new Vec2(v.x - w.x, v.y - w.y); +} + +export class Vec3 { + constructor(public x: number, public y: number, public z: number) {} +} + +export class Mat4 { + static of(...values: readonly number[]): Mat4 { + return new Mat4(new Float32Array(values)); } - static identity(): Matrix4 { + static identity(): Mat4 { // prettier-ignore - return Matrix4.of( + return Mat4.of( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, @@ -45,9 +57,19 @@ export class Matrix4 { } } -export function matrix4_product(a: Matrix4, b: Matrix4): Matrix4 { - const array = new Float32Array(16); +export function mat4_product(a: Mat4, b: Mat4): Mat4 { + const c = new Mat4(new Float32Array(16)); + mat4_product_into_array(c.data, a, b); + return c; +} +export function mat4_multiply(a: Mat4, b: Mat4): void { + const array = new Float32Array(16); + mat4_product_into_array(array, a, b); + a.data.set(array); +} + +function mat4_product_into_array(array: Float32Array, a: Mat4, b: Mat4): void { for (let i = 0; i < 4; i++) { for (let j = 0; j < 4; j++) { for (let k = 0; k < 4; k++) { @@ -55,6 +77,8 @@ export function matrix4_product(a: Matrix4, b: Matrix4): Matrix4 { } } } - - return new Matrix4(array); +} + +export class Quat { + constructor(public x: number, public y: number, public z: number, public w: number) {} } diff --git a/src/core/rendering/Camera.ts b/src/core/rendering/Camera.ts new file mode 100644 index 00000000..63ca5969 --- /dev/null +++ b/src/core/rendering/Camera.ts @@ -0,0 +1,56 @@ +import { Mat4, Vec3 } from "../math"; +import { Mat4Transform, Transform } from "./Transform"; + +export class Camera { + private readonly look_at: Vec3 = new Vec3(0, 0, 0); + private x_rot: number = 0; + private y_rot: number = 0; + private z_rot: number = 0; + private _zoom: number = 1; + private readonly _transform = new Mat4Transform(Mat4.identity()); + + get transform(): Transform { + return this._transform; + } + + pan(x: number, y: number, z: number): this { + this.look_at.x += x; + this.look_at.y += y; + this.look_at.z += z; + this.update_transform(); + return this; + } + + /** + * Increase (or decrease) zoom by a factor. + */ + zoom(factor: number): this { + this._zoom *= factor; + this.look_at.x *= factor; + this.look_at.y *= factor; + this.look_at.z *= factor; + this.update_transform(); + return this; + } + + reset(): this { + this.look_at.x = 0; + this.look_at.y = 0; + this.look_at.z = 0; + this.x_rot = 0; + this.y_rot = 0; + this.z_rot = 0; + this._zoom = 1; + this.update_transform(); + return this; + } + + private update_transform(): void { + this._transform.data[3] = -this.look_at.x; + this._transform.data[7] = -this.look_at.y; + this._transform.data[11] = -this.look_at.z; + this._transform.data[0] = this._zoom; + this._transform.data[5] = this._zoom; + this._transform.data[10] = this._zoom; + } +} diff --git a/src/core/rendering/ShaderProgram.ts b/src/core/rendering/ShaderProgram.ts index 86ffcbae..b12ddee2 100644 --- a/src/core/rendering/ShaderProgram.ts +++ b/src/core/rendering/ShaderProgram.ts @@ -1,6 +1,5 @@ -import { Matrix4 } from "../math"; +import { Mat4 } from "../math"; import { GL, VERTEX_POS_LOC, VERTEX_TEX_LOC } from "./VertexFormat"; -import { Texture } from "./Texture"; export class ShaderProgram { private readonly gl: GL; @@ -51,13 +50,12 @@ export class ShaderProgram { } } - set_transform(matrix: Matrix4): void { + set_transform_uniform(matrix: Mat4): void { this.gl.uniformMatrix4fv(this.transform_loc, true, matrix.data); } - set_texture(texture: Texture): void { - const gl = this.gl; - gl.uniform1i(this.tex_sampler_loc, 0); + set_texture_uniform(unit: GLenum): void { + this.gl.uniform1i(this.tex_sampler_loc, unit - this.gl.TEXTURE0); } bind(): void { diff --git a/src/core/rendering/Transform.ts b/src/core/rendering/Transform.ts index c698759e..4c45cfcf 100644 --- a/src/core/rendering/Transform.ts +++ b/src/core/rendering/Transform.ts @@ -1,15 +1,23 @@ -import { Matrix4 } from "../math"; +import { Mat4 } from "../math"; export interface Transform { - readonly matrix4: Matrix4; + readonly mat4: Mat4; +} + +export class Mat4Transform implements Transform { + readonly data: Float32Array; + + constructor(readonly mat4: Mat4) { + this.data = mat4.data; + } } export class TranslateTransform implements Transform { - readonly matrix4: Matrix4; + readonly mat4: Mat4; constructor(x: number, y: number, z: number) { // prettier-ignore - this.matrix4 = Matrix4.of( + this.mat4 = Mat4.of( 1, 0, 0, x, 0, 1, 0, y, 0, 0, 1, z, @@ -19,11 +27,11 @@ export class TranslateTransform implements Transform { } export class IdentityTransform implements Transform { - readonly matrix4: Matrix4; + readonly mat4: Mat4; constructor() { // prettier-ignore - this.matrix4 = Matrix4.of( + this.mat4 = Mat4.of( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, diff --git a/src/core/rendering/WebglRenderer.ts b/src/core/rendering/WebglRenderer.ts index 63ab3e32..0d38b4b5 100644 --- a/src/core/rendering/WebglRenderer.ts +++ b/src/core/rendering/WebglRenderer.ts @@ -1,5 +1,5 @@ import { Renderer } from "./Renderer"; -import { Matrix4, matrix4_product } from "../math"; +import { Mat4, mat4_product, Vec2, vec2_diff } from "../math"; import { ShaderProgram } from "./ShaderProgram"; import { GL } from "./VertexFormat"; import { Scene } from "./Scene"; @@ -9,17 +9,17 @@ import { POS_TEX_VERTEX_SHADER_SOURCE, POS_VERTEX_SHADER_SOURCE, } from "./shader_sources"; -import { LogManager } from "../Logger"; - -const logger = LogManager.get("core/rendering/WebglRenderer"); +import { Camera } from "./Camera"; export class WebglRenderer extends Renderer { private readonly gl: GL; private readonly shader_programs: ShaderProgram[]; - private render_scheduled = false; - private projection!: Matrix4; + private animation_frame?: number; + private projection!: Mat4; + private pointer_pos?: Vec2; protected readonly scene: Scene; + protected readonly camera = new Camera(); readonly canvas_element: HTMLCanvasElement; @@ -38,6 +38,7 @@ export class WebglRenderer extends Renderer { gl.enable(gl.DEPTH_TEST); gl.enable(gl.CULL_FACE); + gl.clearColor(0.1, 0.1, 0.1, 1); this.shader_programs = [ new ShaderProgram(gl, POS_VERTEX_SHADER_SOURCE, POS_FRAG_SHADER_SOURCE), @@ -48,7 +49,8 @@ export class WebglRenderer extends Renderer { this.set_size(800, 600); - requestAnimationFrame(this.render); + this.canvas_element.addEventListener("mousedown", this.mousedown); + this.canvas_element.addEventListener("wheel", this.wheel, { passive: true }); } dispose(): void { @@ -60,15 +62,21 @@ export class WebglRenderer extends Renderer { } start_rendering(): void { - // TODO + this.schedule_render(); } stop_rendering(): void { - // TODO + if (this.animation_frame != undefined) { + cancelAnimationFrame(this.animation_frame); + } + + this.animation_frame = undefined; } schedule_render = (): void => { - this.render_scheduled = true; + if (this.animation_frame == undefined) { + this.animation_frame = requestAnimationFrame(this.render); + } }; set_size(width: number, height: number): void { @@ -77,32 +85,37 @@ export class WebglRenderer extends Renderer { this.gl.viewport(0, 0, width, height); // prettier-ignore - this.projection = Matrix4.of( + this.projection = Mat4.of( 2/width, 0, 0, 0, 0, 2/height, 0, 0, 0, 0, 2/10, 0, 0, 0, 0, 1, ); + + this.schedule_render(); } private render = (): void => { + this.animation_frame = undefined; const gl = this.gl; gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - this.scene.traverse((node, parent_transform) => { - const transform = matrix4_product(parent_transform, node.transform.matrix4); + const camera_project_mat = mat4_product(this.projection, this.camera.transform.mat4); + + this.scene.traverse((node, parent_mat) => { + const mat = mat4_product(parent_mat, node.transform.mat4); if (node.mesh) { const program = this.shader_programs[node.mesh.format]; program.bind(); - program.set_transform(transform); + program.set_transform_uniform(mat); if (node.mesh.texture) { gl.activeTexture(gl.TEXTURE0); node.mesh.texture.bind(gl); - program.set_texture(node.mesh.texture); + program.set_texture_uniform(gl.TEXTURE0); } node.mesh.render(gl); @@ -111,9 +124,43 @@ export class WebglRenderer extends Renderer { program.unbind(); } - return transform; - }, this.projection); + return mat; + }, camera_project_mat); + }; - requestAnimationFrame(this.render); + private mousedown = (evt: MouseEvent): void => { + if (evt.buttons === 1) { + this.pointer_pos = new Vec2(evt.clientX, evt.clientY); + + window.addEventListener("mousemove", this.mousemove); + window.addEventListener("mouseup", this.mouseup); + } + }; + + private mousemove = (evt: MouseEvent): void => { + if (evt.buttons === 1) { + const new_pos = new Vec2(evt.clientX, evt.clientY); + const diff = vec2_diff(new_pos, this.pointer_pos!); + this.camera.pan(-diff.x, diff.y, 0); + this.pointer_pos = new_pos; + this.schedule_render(); + } + }; + + private mouseup = (): void => { + this.pointer_pos = undefined; + + window.removeEventListener("mousemove", this.mousemove); + window.removeEventListener("mouseup", this.mouseup); + }; + + private wheel = (evt: WheelEvent): void => { + if (evt.deltaY < 0) { + this.camera.zoom(1.1); + } else { + this.camera.zoom(0.9); + } + + this.schedule_render(); }; } diff --git a/src/viewer/index.ts b/src/viewer/index.ts index 5ad9a839..6cfe606c 100644 --- a/src/viewer/index.ts +++ b/src/viewer/index.ts @@ -60,8 +60,8 @@ export function initialize_viewer( let renderer: Renderer; if (gui_store.feature_active("renderer")) { - const { WebglTextureRenderer } = await import("./rendering/WebglTextureRenderer"); - renderer = new WebglTextureRenderer(controller); + const { TextureWebglRenderer } = await import("./rendering/TextureWebglRenderer"); + renderer = new TextureWebglRenderer(controller); } else { const { TextureRenderer } = await import("./rendering/TextureRenderer"); renderer = new TextureRenderer(controller, create_three_renderer()); diff --git a/src/viewer/rendering/WebglTextureRenderer.ts b/src/viewer/rendering/TextureWebglRenderer.ts similarity index 92% rename from src/viewer/rendering/WebglTextureRenderer.ts rename to src/viewer/rendering/TextureWebglRenderer.ts index eaedbbce..8de28bce 100644 --- a/src/viewer/rendering/WebglTextureRenderer.ts +++ b/src/viewer/rendering/TextureWebglRenderer.ts @@ -10,7 +10,7 @@ import { Texture, TextureFormat } from "../../core/rendering/Texture"; const logger = LogManager.get("viewer/rendering/WebglTextureRenderer"); -export class WebglTextureRenderer extends WebglRenderer { +export class TextureWebglRenderer extends WebglRenderer { private readonly disposer = new Disposer(); constructor(ctrl: TextureController) { @@ -18,7 +18,9 @@ export class WebglTextureRenderer extends WebglRenderer { this.disposer.add_all( ctrl.textures.observe(({ value: textures }) => { - this.render_textures(textures); + this.scene.delete(); + this.camera.reset(); + this.create_quads(textures); this.schedule_render(); }), ); @@ -29,9 +31,7 @@ export class WebglTextureRenderer extends WebglRenderer { this.disposer.dispose(); } - private render_textures(textures: readonly XvrTexture[]): void { - this.scene.delete(); - + private create_quads(textures: readonly XvrTexture[]): void { let total_width = 10 * (textures.length - 1); // 10px spacing between textures. let total_height = 0;