Added experimental WebGL renderer.

This commit is contained in:
Daan Vanden Bosch 2020-01-19 17:16:28 +01:00
parent f4d9cb290e
commit 85ccdbb0a6
26 changed files with 961 additions and 181 deletions

View File

@ -5,7 +5,7 @@ import { create_item_type_stores } from "../core/stores/ItemTypeStore";
import { create_item_drop_stores } from "../hunt_optimizer/stores/ItemDropStore";
import { ApplicationView } from "./gui/ApplicationView";
import { throttle } from "lodash";
import { DisposableThreeRenderer } from "../core/rendering/Renderer";
import { DisposableThreeRenderer } from "../core/rendering/ThreeRenderer";
import { Disposer } from "../core/observable/Disposer";
import { disposable_custom_listener, disposable_listener } from "../core/gui/dom";
import { Random } from "../core/Random";

View File

@ -1,7 +1,7 @@
import { ResizableWidget } from "./ResizableWidget";
import { Renderer } from "../rendering/Renderer";
import { div } from "./dom";
import { Widget } from "./Widget";
import { Renderer } from "../rendering/Renderer";
export class RendererWidget extends ResizableWidget {
readonly element = div({ className: "core_RendererWidget" });

View File

@ -1,24 +0,0 @@
const TO_DEG = 180 / Math.PI;
const TO_RAD = 1 / TO_DEG;
/**
* Converts radians to degrees.
*/
export function rad_to_deg(rad: number): number {
return rad * TO_DEG;
}
/**
* Converts degrees to radians.
*/
export function deg_to_rad(deg: number): number {
return deg * TO_RAD;
}
/**
* @returns the floored modulus of its arguments. The computed value will have the same sign as the
* `divisor`.
*/
export function floor_mod(dividend: number, divisor: number): number {
return ((dividend % divisor) + divisor) % divisor;
}

60
src/core/math/index.ts Normal file
View File

@ -0,0 +1,60 @@
import { assert } from "../util";
const TO_DEG = 180 / Math.PI;
const TO_RAD = 1 / TO_DEG;
/**
* Converts radians to degrees.
*/
export function rad_to_deg(rad: number): number {
return rad * TO_DEG;
}
/**
* Converts degrees to radians.
*/
export function deg_to_rad(deg: number): number {
return deg * TO_RAD;
}
/**
* @returns the floored modulus of its arguments. The computed value will have the same sign as the
* `divisor`.
*/
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));
}
static identity(): Matrix4 {
// prettier-ignore
return Matrix4.of(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
)
}
constructor(readonly data: Float32Array) {
assert(data.length === 16, "values should be of length 16.");
}
}
export function matrix4_product(a: Matrix4, b: Matrix4): Matrix4 {
const array = new Float32Array(16);
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
for (let k = 0; k < 4; k++) {
array[i * 4 + j] += a.data[i * 4 + k] * b.data[k * 4 + j];
}
}
}
return new Matrix4(array);
}

162
src/core/rendering/Mesh.ts Normal file
View File

@ -0,0 +1,162 @@
import {
GL,
vertex_format_size,
vertex_format_tex_offset,
VERTEX_POS_LOC,
VERTEX_TEX_LOC,
VertexFormat,
} from "./VertexFormat";
import { assert } from "../util";
import { Texture } from "./Texture";
export class Mesh {
private readonly index_count: number;
private vao: WebGLVertexArrayObject | null = null;
private vertex_buffer: WebGLBuffer | null = null;
private index_buffer: WebGLBuffer | null = null;
private uploaded = false;
constructor(
readonly format: VertexFormat,
private readonly vertex_data: ArrayBuffer,
private readonly index_data: ArrayBuffer,
readonly texture?: Texture,
) {
this.index_count = index_data.byteLength / 2;
}
upload(gl: GL): void {
if (this.uploaded) return;
try {
this.vao = gl.createVertexArray();
if (this.vao == null) throw new Error("Failed to create VAO.");
this.vertex_buffer = gl.createBuffer();
if (this.vertex_buffer == null) throw new Error("Failed to create vertex buffer.");
this.index_buffer = gl.createBuffer();
if (this.index_buffer == null) throw new Error("Failed to create index buffer.");
gl.bindVertexArray(this.vao);
// Vertex data.
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertex_buffer);
gl.bufferData(gl.ARRAY_BUFFER, this.vertex_data, gl.STATIC_DRAW);
const vertex_size = vertex_format_size(this.format);
gl.vertexAttribPointer(VERTEX_POS_LOC, 3, gl.FLOAT, true, vertex_size, 0);
gl.enableVertexAttribArray(VERTEX_POS_LOC);
const tex_offset = vertex_format_tex_offset(this.format);
if (tex_offset !== -1) {
gl.vertexAttribPointer(
VERTEX_TEX_LOC,
2,
gl.UNSIGNED_SHORT,
true,
vertex_size,
tex_offset,
);
gl.enableVertexAttribArray(VERTEX_TEX_LOC);
}
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// Index data.
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.index_buffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.index_data, gl.STATIC_DRAW);
gl.bindVertexArray(null);
this.texture?.upload(gl);
this.uploaded = true;
} catch (e) {
gl.deleteVertexArray(this.vao);
this.vao = null;
gl.deleteBuffer(this.vertex_buffer);
this.vertex_buffer = null;
gl.deleteBuffer(this.index_buffer);
this.index_buffer = null;
throw e;
}
}
render(gl: GL): void {
gl.bindVertexArray(this.vao);
gl.drawElements(gl.TRIANGLES, this.index_count, gl.UNSIGNED_SHORT, 0);
gl.bindVertexArray(null);
}
delete(gl: GL): void {
gl.deleteVertexArray(this.vao);
gl.deleteBuffer(this.vertex_buffer);
gl.deleteBuffer(this.index_buffer);
}
}
export class MeshBuilder {
private readonly vertex_data: {
x: number;
y: number;
z: number;
u?: number;
v?: number;
}[] = [];
private readonly index_data: number[] = [];
private _texture?: Texture;
constructor(private readonly format: VertexFormat) {}
vertex(x: number, y: number, z: number, u?: number, v?: number): this {
switch (this.format) {
case VertexFormat.PosTex:
assert(
u != undefined && v != undefined,
`Vertex format ${VertexFormat[this.format]} requires texture coordinates.`,
);
break;
}
this.vertex_data.push({ x, y, z, u, v });
return this;
}
triangle(v1: number, v2: number, v3: number): this {
this.index_data.push(v1, v2, v3);
return this;
}
texture(tex: Texture): this {
this._texture = tex;
return this;
}
build(): Mesh {
const v_size = vertex_format_size(this.format);
const v_tex_offset = vertex_format_tex_offset(this.format);
const v_data = new ArrayBuffer(this.vertex_data.length * v_size);
const v_view = new DataView(v_data);
let i = 0;
for (const { x, y, z, u, v } of this.vertex_data) {
v_view.setFloat32(i, x, true);
v_view.setFloat32(i + 4, y, true);
v_view.setFloat32(i + 8, z, true);
if (v_tex_offset !== -1) {
v_view.setUint16(i + v_tex_offset, u! * 0xffff, true);
v_view.setUint16(i + v_tex_offset + 2, v! * 0xffff, true);
}
i += v_size;
}
return new Mesh(this.format, v_data, new Uint16Array(this.index_data), this._texture);
}
}

View File

@ -1,137 +1,13 @@
import CameraControls from "camera-controls";
import * as THREE from "three";
import {
Clock,
Color,
Group,
HemisphereLight,
OrthographicCamera,
PerspectiveCamera,
Scene,
Vector2,
Vector3,
} from "three";
import { Disposable } from "../observable/Disposable";
CameraControls.install({
// Hack to make panning and orbiting work the way we want.
THREE: {
...THREE,
MOUSE: { ...THREE.MOUSE, LEFT: THREE.MOUSE.RIGHT, RIGHT: THREE.MOUSE.LEFT },
},
});
export interface DisposableThreeRenderer extends THREE.Renderer, Disposable {}
export abstract class Renderer implements Disposable {
private _debug = false;
abstract readonly canvas_element: HTMLCanvasElement;
get debug(): boolean {
return this._debug;
}
abstract dispose(): void;
set debug(debug: boolean) {
this._debug = debug;
}
abstract start_rendering(): void;
abstract readonly camera: PerspectiveCamera | OrthographicCamera;
readonly controls!: CameraControls;
readonly scene = new Scene();
readonly light_holder = new Group();
abstract stop_rendering(): void;
private readonly renderer: DisposableThreeRenderer;
private render_scheduled = false;
private animation_frame_handle?: number = undefined;
private readonly light = new HemisphereLight(0xffffff, 0x505050, 1.0);
private readonly controls_clock = new Clock();
private readonly size = new Vector2(0, 0);
protected constructor(three_renderer: DisposableThreeRenderer) {
this.renderer = three_renderer;
this.renderer.domElement.tabIndex = 0;
this.renderer.domElement.addEventListener("mousedown", this.on_mouse_down);
this.renderer.domElement.style.outline = "none";
this.scene.background = new Color(0x181818);
this.light_holder.add(this.light);
this.scene.add(this.light_holder);
}
get canvas_element(): HTMLCanvasElement {
return this.renderer.domElement;
}
set_size(width: number, height: number): void {
this.size.set(width, height);
this.renderer.setSize(width, height);
this.schedule_render();
}
pointer_pos_to_device_coords(pos: Vector2): void {
pos.set((pos.x / this.size.width) * 2 - 1, (pos.y / this.size.height) * -2 + 1);
}
start_rendering(): void {
if (this.animation_frame_handle == undefined) {
this.schedule_render();
this.animation_frame_handle = requestAnimationFrame(this.call_render);
}
}
stop_rendering(): void {
if (this.animation_frame_handle != undefined) {
cancelAnimationFrame(this.animation_frame_handle);
this.animation_frame_handle = undefined;
}
}
schedule_render = (): void => {
this.render_scheduled = true;
};
reset_camera(position: Vector3, look_at: Vector3): void {
this.controls.setLookAt(
position.x,
position.y,
position.z,
look_at.x,
look_at.y,
look_at.z,
);
}
dispose(): void {
this.renderer.dispose();
this.controls.dispose();
}
protected init_camera_controls(): void {
(this.controls as CameraControls) = new CameraControls(
this.camera,
this.renderer.domElement,
);
this.controls.dampingFactor = 1;
this.controls.draggingDampingFactor = 1;
}
protected render(): void {
this.renderer.render(this.scene, this.camera);
}
private on_mouse_down = (e: Event): void => {
if (e.currentTarget) (e.currentTarget as HTMLElement).focus();
};
private call_render = (): void => {
const controls_updated = this.controls.update(this.controls_clock.getDelta());
const should_render = this.render_scheduled || controls_updated;
this.render_scheduled = false;
if (should_render) {
this.render();
}
this.animation_frame_handle = requestAnimationFrame(this.call_render);
};
abstract set_size(width: number, height: number): void;
}

View File

@ -0,0 +1,54 @@
import { Mesh } from "./Mesh";
import { IdentityTransform, Transform } from "./Transform";
import { GL } from "./VertexFormat";
export class Scene {
readonly root_node = new Node(undefined, new IdentityTransform());
constructor(private readonly gl: GL) {}
/**
* Creates a new node with `node` as parent. Takes ownership of `mesh`.
*
* @param node - The parent node.
* @param mesh - The new node's mesh.
* @param transform - The new node's transform.
*/
add_child(node: Node, mesh: Mesh, transform: Transform): this {
node.children.push(new Node(mesh, transform));
mesh.upload(this.gl);
return this;
}
/**
* Deletes all GL objects related to this scene and resets the scene.
*/
delete(): void {
this.traverse(node => {
node.mesh?.texture?.delete(this.gl);
node.mesh?.delete(this.gl);
node.mesh = undefined;
}, undefined);
this.root_node.children.splice(0);
this.root_node.transform = new IdentityTransform();
}
traverse<T>(f: (node: Node, data: T) => T, data: T): void {
this.traverse_node(this.root_node, f, data);
}
private traverse_node<T>(node: Node, f: (node: Node, data: T) => T, data: T): void {
const child_data = f(node, data);
for (const child of node.children) {
this.traverse_node(child, f, child_data);
}
}
}
export class Node {
readonly children: Node[] = [];
constructor(public mesh: Mesh | undefined, public transform: Transform) {}
}

View File

@ -0,0 +1,91 @@
import { Matrix4 } from "../math";
import { GL, VERTEX_POS_LOC, VERTEX_TEX_LOC } from "./VertexFormat";
import { Texture } from "./Texture";
export class ShaderProgram {
private readonly gl: GL;
private readonly program: WebGLProgram;
private readonly transform_loc: WebGLUniformLocation;
private readonly tex_sampler_loc: WebGLUniformLocation | null;
constructor(gl: GL, vertex_source: string, frag_source: string) {
this.gl = gl;
const program = gl.createProgram();
if (program == null) throw new Error("Failed to create program.");
this.program = program;
let vertex_shader: WebGLShader | null = null;
let frag_shader: WebGLShader | null = null;
try {
vertex_shader = create_shader(gl, gl.VERTEX_SHADER, vertex_source);
gl.attachShader(program, vertex_shader);
frag_shader = create_shader(gl, gl.FRAGMENT_SHADER, frag_source);
gl.attachShader(program, frag_shader);
gl.bindAttribLocation(program, VERTEX_POS_LOC, "pos");
gl.bindAttribLocation(program, VERTEX_TEX_LOC, "tex");
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const log = gl.getProgramInfoLog(program);
throw new Error("Shader linking failed. Program log:\n" + log);
}
const transform_loc = gl.getUniformLocation(program, "transform");
if (transform_loc == null) throw new Error("Couldn't get transform uniform location.");
this.transform_loc = transform_loc;
this.tex_sampler_loc = gl.getUniformLocation(program, "tex_sampler");
gl.detachShader(program, vertex_shader);
gl.detachShader(program, frag_shader);
} catch (e) {
gl.deleteProgram(program);
throw e;
} finally {
// Always delete shaders after we're done.
gl.deleteShader(vertex_shader);
gl.deleteShader(frag_shader);
}
}
set_transform(matrix: Matrix4): 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);
}
bind(): void {
this.gl.useProgram(this.program);
}
unbind(): void {
this.gl.useProgram(null);
}
delete(): void {
this.gl.deleteProgram(this.program);
}
}
function create_shader(gl: GL, type: GLenum, source: string): WebGLShader {
const shader = gl.createShader(type);
if (shader == null) throw new Error(`Failed to create shader of type ${type}.`);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const log = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new Error("Vertex shader compilation failed. Shader log:\n" + log);
}
return shader;
}

View File

@ -0,0 +1,72 @@
import { GL } from "./VertexFormat";
export enum TextureFormat {
RGBA_S3TC_DXT1,
RGBA_S3TC_DXT3,
}
export class Texture {
private uploaded = false;
private texture: WebGLTexture | null = null;
constructor(
private readonly width: number,
private readonly height: number,
private readonly format: TextureFormat,
private readonly data: ArrayBuffer,
) {}
upload(gl: GL): void {
if (this.uploaded) return;
const ext = gl.getExtension("WEBGL_compressed_texture_s3tc");
if (!ext) {
throw new Error("Extension WEBGL_compressed_texture_s3tc not supported.");
}
const texture = gl.createTexture();
if (texture == null) throw new Error("Failed to create texture.");
this.texture = texture;
let gl_format: GLenum;
switch (this.format) {
case TextureFormat.RGBA_S3TC_DXT1:
gl_format = ext.COMPRESSED_RGBA_S3TC_DXT1_EXT;
break;
case TextureFormat.RGBA_S3TC_DXT3:
gl_format = ext.COMPRESSED_RGBA_S3TC_DXT3_EXT;
break;
}
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.compressedTexImage2D(
gl.TEXTURE_2D,
0,
gl_format,
this.width,
this.height,
0,
new Uint8Array(this.data),
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.bindTexture(gl.TEXTURE_2D, null);
this.uploaded = true;
}
bind(gl: GL): void {
gl.bindTexture(gl.TEXTURE_2D, this.texture);
}
unbind(gl: GL): void {
gl.bindTexture(gl.TEXTURE_2D, null);
}
delete(gl: GL): void {
gl.deleteTexture(this.texture);
}
}

View File

@ -0,0 +1,142 @@
import CameraControls from "camera-controls";
import * as THREE from "three";
import {
Clock,
Color,
Group,
HemisphereLight,
OrthographicCamera,
PerspectiveCamera,
Scene,
Vector2,
Vector3,
} from "three";
import { Disposable } from "../observable/Disposable";
import { Renderer } from "./Renderer";
CameraControls.install({
// Hack to make panning and orbiting work the way we want.
THREE: {
...THREE,
MOUSE: { ...THREE.MOUSE, LEFT: THREE.MOUSE.RIGHT, RIGHT: THREE.MOUSE.LEFT },
},
});
export interface DisposableThreeRenderer extends THREE.Renderer, Disposable {}
/**
* Uses THREE.js for rendering.
*/
export abstract class ThreeRenderer extends Renderer {
private _debug = false;
get debug(): boolean {
return this._debug;
}
set debug(debug: boolean) {
this._debug = debug;
}
abstract readonly camera: PerspectiveCamera | OrthographicCamera;
readonly controls!: CameraControls;
readonly scene = new Scene();
readonly light_holder = new Group();
private readonly renderer: DisposableThreeRenderer;
private render_scheduled = false;
private animation_frame_handle?: number = undefined;
private readonly light = new HemisphereLight(0xffffff, 0x505050, 1.0);
private readonly controls_clock = new Clock();
private readonly size = new Vector2(0, 0);
protected constructor(three_renderer: DisposableThreeRenderer) {
super();
this.renderer = three_renderer;
this.renderer.domElement.tabIndex = 0;
this.renderer.domElement.addEventListener("mousedown", this.on_mouse_down);
this.renderer.domElement.style.outline = "none";
this.scene.background = new Color(0x181818);
this.light_holder.add(this.light);
this.scene.add(this.light_holder);
}
get canvas_element(): HTMLCanvasElement {
return this.renderer.domElement;
}
set_size(width: number, height: number): void {
this.size.set(width, height);
this.renderer.setSize(width, height);
this.schedule_render();
}
pointer_pos_to_device_coords(pos: Vector2): void {
pos.set((pos.x / this.size.width) * 2 - 1, (pos.y / this.size.height) * -2 + 1);
}
start_rendering(): void {
if (this.animation_frame_handle == undefined) {
this.schedule_render();
this.animation_frame_handle = requestAnimationFrame(this.call_render);
}
}
stop_rendering(): void {
if (this.animation_frame_handle != undefined) {
cancelAnimationFrame(this.animation_frame_handle);
this.animation_frame_handle = undefined;
}
}
schedule_render = (): void => {
this.render_scheduled = true;
};
reset_camera(position: Vector3, look_at: Vector3): void {
this.controls.setLookAt(
position.x,
position.y,
position.z,
look_at.x,
look_at.y,
look_at.z,
);
}
dispose(): void {
this.renderer.dispose();
this.controls.dispose();
}
protected init_camera_controls(): void {
(this.controls as CameraControls) = new CameraControls(
this.camera,
this.renderer.domElement,
);
this.controls.dampingFactor = 1;
this.controls.draggingDampingFactor = 1;
}
protected render(): void {
this.renderer.render(this.scene, this.camera);
}
private on_mouse_down = (e: Event): void => {
if (e.currentTarget) (e.currentTarget as HTMLElement).focus();
};
private call_render = (): void => {
const controls_updated = this.controls.update(this.controls_clock.getDelta());
const should_render = this.render_scheduled || controls_updated;
this.render_scheduled = false;
if (should_render) {
this.render();
}
this.animation_frame_handle = requestAnimationFrame(this.call_render);
};
}

View File

@ -0,0 +1,33 @@
import { Matrix4 } from "../math";
export interface Transform {
readonly matrix4: Matrix4;
}
export class TranslateTransform implements Transform {
readonly matrix4: Matrix4;
constructor(x: number, y: number, z: number) {
// prettier-ignore
this.matrix4 = Matrix4.of(
1, 0, 0, x,
0, 1, 0, y,
0, 0, 1, z,
0, 0, 0, 1,
);
}
}
export class IdentityTransform implements Transform {
readonly matrix4: Matrix4;
constructor() {
// prettier-ignore
this.matrix4 = Matrix4.of(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
);
}
}

View File

@ -0,0 +1,27 @@
export type GL = WebGL2RenderingContext;
export enum VertexFormat {
Pos,
PosTex,
}
export const VERTEX_POS_LOC = 0;
export const VERTEX_TEX_LOC = 1;
export function vertex_format_size(format: VertexFormat): number {
switch (format) {
case VertexFormat.Pos:
return 12;
case VertexFormat.PosTex:
return 16;
}
}
export function vertex_format_tex_offset(format: VertexFormat): number {
switch (format) {
case VertexFormat.Pos:
return -1;
case VertexFormat.PosTex:
return 12;
}
}

View File

@ -0,0 +1,119 @@
import { Renderer } from "./Renderer";
import { Matrix4, matrix4_product } from "../math";
import { ShaderProgram } from "./ShaderProgram";
import { GL } from "./VertexFormat";
import { Scene } from "./Scene";
import {
POS_FRAG_SHADER_SOURCE,
POS_TEX_FRAG_SHADER_SOURCE,
POS_TEX_VERTEX_SHADER_SOURCE,
POS_VERTEX_SHADER_SOURCE,
} from "./shader_sources";
import { LogManager } from "../Logger";
const logger = LogManager.get("core/rendering/WebglRenderer");
export class WebglRenderer extends Renderer {
private readonly gl: GL;
private readonly shader_programs: ShaderProgram[];
private render_scheduled = false;
private projection!: Matrix4;
protected readonly scene: Scene;
readonly canvas_element: HTMLCanvasElement;
constructor() {
super();
this.canvas_element = document.createElement("canvas");
const gl = this.canvas_element.getContext("webgl2");
if (gl == null) {
throw new Error("Failed to initialize webgl2 context.");
}
this.gl = gl;
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
this.shader_programs = [
new ShaderProgram(gl, POS_VERTEX_SHADER_SOURCE, POS_FRAG_SHADER_SOURCE),
new ShaderProgram(gl, POS_TEX_VERTEX_SHADER_SOURCE, POS_TEX_FRAG_SHADER_SOURCE),
];
this.scene = new Scene(gl);
this.set_size(800, 600);
requestAnimationFrame(this.render);
}
dispose(): void {
for (const program of this.shader_programs) {
program.delete();
}
this.scene.delete();
}
start_rendering(): void {
// TODO
}
stop_rendering(): void {
// TODO
}
schedule_render = (): void => {
this.render_scheduled = true;
};
set_size(width: number, height: number): void {
this.canvas_element.width = width;
this.canvas_element.height = height;
this.gl.viewport(0, 0, width, height);
// prettier-ignore
this.projection = Matrix4.of(
2/width, 0, 0, 0,
0, 2/height, 0, 0,
0, 0, 2/10, 0,
0, 0, 0, 1,
);
}
private render = (): void => {
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);
if (node.mesh) {
const program = this.shader_programs[node.mesh.format];
program.bind();
program.set_transform(transform);
if (node.mesh.texture) {
gl.activeTexture(gl.TEXTURE0);
node.mesh.texture.bind(gl);
program.set_texture(node.mesh.texture);
}
node.mesh.render(gl);
node.mesh.texture?.unbind(gl);
program.unbind();
}
return transform;
}, this.projection);
requestAnimationFrame(this.render);
};
}

View File

@ -0,0 +1,55 @@
export const POS_VERTEX_SHADER_SOURCE = `#version 300 es
precision mediump float;
uniform mat4 transform;
in vec4 pos;
void main() {
gl_Position = transform * pos;
}
`;
export const POS_FRAG_SHADER_SOURCE = `#version 300 es
precision mediump float;
out vec4 frag_color;
void main() {
frag_color = vec4(0, 1, 1, 1);
}
`;
export const POS_TEX_VERTEX_SHADER_SOURCE = `#version 300 es
precision mediump float;
uniform mat4 transform;
in vec4 pos;
in vec2 tex;
out vec2 f_tex;
void main() {
gl_Position = transform * pos;
f_tex = tex;
}
`;
export const POS_TEX_FRAG_SHADER_SOURCE = `#version 300 es
precision mediump float;
uniform sampler2D tex_sampler;
in vec2 f_tex;
out vec4 frag_color;
void main() {
frag_color = texture(tex_sampler, f_tex);
}
`;

View File

@ -9,7 +9,7 @@ import "@fortawesome/fontawesome-free/js/brands";
import { initialize_application } from "./application";
import { FetchClient } from "./core/HttpClient";
import { WebGLRenderer } from "three";
import { DisposableThreeRenderer } from "./core/rendering/Renderer";
import { DisposableThreeRenderer } from "./core/rendering/ThreeRenderer";
import { Random } from "./core/Random";
import { DateClock } from "./core/Clock";

View File

@ -5,7 +5,7 @@ import { QuestRendererView } from "./QuestRendererView";
import { QuestEntityControls } from "../rendering/QuestEntityControls";
import { AreaAssetLoader } from "../loading/AreaAssetLoader";
import { EntityAssetLoader } from "../loading/EntityAssetLoader";
import { DisposableThreeRenderer } from "../../core/rendering/Renderer";
import { DisposableThreeRenderer } from "../../core/rendering/ThreeRenderer";
export class QuestEditorRendererView extends QuestRendererView {
private readonly entity_controls: QuestEntityControls;

View File

@ -4,7 +4,7 @@ import { QuestRendererView } from "./QuestRendererView";
import { QuestEditorStore } from "../stores/QuestEditorStore";
import { AreaAssetLoader } from "../loading/AreaAssetLoader";
import { EntityAssetLoader } from "../loading/EntityAssetLoader";
import { DisposableThreeRenderer } from "../../core/rendering/Renderer";
import { DisposableThreeRenderer } from "../../core/rendering/ThreeRenderer";
export class QuestRunnerRendererView extends QuestRendererView {
constructor(

View File

@ -7,7 +7,7 @@ import { AreaAssetLoader } from "./loading/AreaAssetLoader";
import { HttpClient } from "../core/HttpClient";
import { EntityImageRenderer } from "./rendering/EntityImageRenderer";
import { EntityAssetLoader } from "./loading/EntityAssetLoader";
import { DisposableThreeRenderer } from "../core/rendering/Renderer";
import { DisposableThreeRenderer } from "../core/rendering/ThreeRenderer";
import { QuestEditorUiPersister } from "./persistence/QuestEditorUiPersister";
import { QuestEditorToolBarView } from "./gui/QuestEditorToolBarView";
import { QuestEditorToolBarController } from "./controllers/QuestEditorToolBarController";

View File

@ -4,7 +4,7 @@ import { create_entity_type_mesh } from "./conversion/entities";
import { sequential } from "../../core/sequential";
import { EntityAssetLoader } from "../loading/EntityAssetLoader";
import { Disposable } from "../../core/observable/Disposable";
import { DisposableThreeRenderer } from "../../core/rendering/Renderer";
import { DisposableThreeRenderer } from "../../core/rendering/ThreeRenderer";
import { LoadingCache } from "../loading/LoadingCache";
import { DisposablePromise } from "../../core/DisposablePromise";

View File

@ -1,4 +1,4 @@
import { DisposableThreeRenderer, Renderer } from "../../core/rendering/Renderer";
import { DisposableThreeRenderer, ThreeRenderer } from "../../core/rendering/ThreeRenderer";
import { Group, Mesh, MeshLambertMaterial, Object3D, PerspectiveCamera } from "three";
import { QuestEntityModel } from "../model/QuestEntityModel";
import { Quest3DModelManager } from "./Quest3DModelManager";
@ -6,7 +6,7 @@ import { Disposer } from "../../core/observable/Disposer";
import { ColorType, EntityUserData, NPC_COLORS, OBJECT_COLORS } from "./conversion/entities";
import { QuestNpcModel } from "../model/QuestNpcModel";
export class QuestRenderer extends Renderer {
export class QuestRenderer extends ThreeRenderer {
private _collision_geometry = new Object3D();
private _render_geometry = new Object3D();
private _entity_models = new Object3D();

View File

@ -2,10 +2,10 @@ import { div, Icon } from "../../core/gui/dom";
import { FileButton } from "../../core/gui/FileButton";
import { ToolBar } from "../../core/gui/ToolBar";
import { RendererWidget } from "../../core/gui/RendererWidget";
import { TextureRenderer } from "../rendering/TextureRenderer";
import { ResizableView } from "../../core/gui/ResizableView";
import { TextureController } from "../controllers/TextureController";
import { ResultDialog } from "../../core/gui/ResultDialog";
import { Renderer } from "../../core/rendering/Renderer";
export class TextureView extends ResizableView {
readonly element = div({ className: "viewer_TextureView" });
@ -20,7 +20,7 @@ export class TextureView extends ResizableView {
private readonly renderer_view: RendererWidget;
constructor(ctrl: TextureController, renderer: TextureRenderer) {
constructor(ctrl: TextureController, renderer: Renderer) {
super();
this.renderer_view = this.add(new RendererWidget(renderer));

View File

@ -1,17 +1,11 @@
import { ViewerView } from "./gui/ViewerView";
import { GuiStore } from "../core/stores/GuiStore";
import { HttpClient } from "../core/HttpClient";
import { DisposableThreeRenderer } from "../core/rendering/Renderer";
import { DisposableThreeRenderer } from "../core/rendering/ThreeRenderer";
import { Disposable } from "../core/observable/Disposable";
import { Disposer } from "../core/observable/Disposer";
import { TextureRenderer } from "./rendering/TextureRenderer";
import { ModelRenderer } from "./rendering/ModelRenderer";
import { Random } from "../core/Random";
import { ModelToolBarView } from "./gui/model/ModelToolBarView";
import { ModelStore } from "./stores/ModelStore";
import { ModelToolBarController } from "./controllers/model/ModelToolBarController";
import { CharacterClassOptionsView } from "./gui/model/CharacterClassOptionsView";
import { CharacterClassOptionsController } from "./controllers/model/CharacterClassOptionsController";
import { Renderer } from "../core/rendering/Renderer";
export function initialize_viewer(
http_client: HttpClient,
@ -26,10 +20,23 @@ export function initialize_viewer(
async () => {
const { ModelController } = await import("./controllers/model/ModelController");
const { ModelRenderer } = await import("./rendering/ModelRenderer");
const { ModelView } = await import("./gui/model/ModelView");
const { CharacterClassAssetLoader } = await import(
"./loading/CharacterClassAssetLoader"
);
const { ModelToolBarView } = await import("./gui/model/ModelToolBarView");
const { ModelStore } = await import("./stores/ModelStore");
const { ModelToolBarController } = await import(
"./controllers/model/ModelToolBarController"
);
const { CharacterClassOptionsView } = await import(
"./gui/model/CharacterClassOptionsView"
);
const { CharacterClassOptionsController } = await import(
"./controllers/model/CharacterClassOptionsController"
);
const asset_loader = disposer.add(new CharacterClassAssetLoader(http_client));
const store = disposer.add(new ModelStore(gui_store, asset_loader, random));
const model_controller = new ModelController(store);
@ -47,12 +54,20 @@ export function initialize_viewer(
async () => {
const { TextureController } = await import("./controllers/TextureController");
const { TextureView } = await import("./gui/TextureView");
const controller = disposer.add(new TextureController());
return new TextureView(
controller,
new TextureRenderer(controller, create_three_renderer()),
);
let renderer: Renderer;
if (gui_store.feature_active("renderer")) {
const { WebglTextureRenderer } = await import("./rendering/WebglTextureRenderer");
renderer = new WebglTextureRenderer(controller);
} else {
const { TextureRenderer } = await import("./rendering/TextureRenderer");
renderer = new TextureRenderer(controller, create_three_renderer());
}
return new TextureView(controller, renderer);
},
);

View File

@ -19,7 +19,7 @@ import {
create_animation_clip,
PSO_FRAME_RATE,
} from "../../core/rendering/conversion/ninja_animation";
import { DisposableThreeRenderer, Renderer } from "../../core/rendering/Renderer";
import { DisposableThreeRenderer, ThreeRenderer } from "../../core/rendering/ThreeRenderer";
import { Disposer } from "../../core/observable/Disposer";
import { ChangeEvent } from "../../core/observable/Observable";
import { LogManager } from "../../core/Logger";
@ -40,7 +40,7 @@ const DEFAULT_SKINNED_MATERIAL = new MeshLambertMaterial({
const CAMERA_POSITION = Object.freeze(new Vector3(0, 10, 20));
const CAMERA_LOOK_AT = Object.freeze(new Vector3(0, 0, 0));
export class ModelRenderer extends Renderer implements Disposable {
export class ModelRenderer extends ThreeRenderer implements Disposable {
private readonly disposer = new Disposer();
private readonly clock = new Clock();
private character_class_active: boolean;

View File

@ -8,7 +8,7 @@ import {
Vector3,
} from "three";
import { Disposable } from "../../core/observable/Disposable";
import { DisposableThreeRenderer, Renderer } from "../../core/rendering/Renderer";
import { DisposableThreeRenderer, ThreeRenderer } from "../../core/rendering/ThreeRenderer";
import { Disposer } from "../../core/observable/Disposer";
import { XvrTexture } from "../../core/data_formats/parsing/ninja/texture";
import { xvr_texture_to_texture } from "../../core/rendering/conversion/ninja_textures";
@ -17,7 +17,7 @@ import { TextureController } from "../controllers/TextureController";
const logger = LogManager.get("viewer/rendering/TextureRenderer");
export class TextureRenderer extends Renderer implements Disposable {
export class TextureRenderer extends ThreeRenderer implements Disposable {
private readonly disposer = new Disposer();
private readonly quad_meshes: Mesh[] = [];

View File

@ -0,0 +1,98 @@
import { Disposer } from "../../core/observable/Disposer";
import { LogManager } from "../../core/Logger";
import { TextureController } from "../controllers/TextureController";
import { WebglRenderer } from "../../core/rendering/WebglRenderer";
import { XvrTexture } from "../../core/data_formats/parsing/ninja/texture";
import { Mesh, MeshBuilder } from "../../core/rendering/Mesh";
import { TranslateTransform } from "../../core/rendering/Transform";
import { VertexFormat } from "../../core/rendering/VertexFormat";
import { Texture, TextureFormat } from "../../core/rendering/Texture";
const logger = LogManager.get("viewer/rendering/WebglTextureRenderer");
export class WebglTextureRenderer extends WebglRenderer {
private readonly disposer = new Disposer();
constructor(ctrl: TextureController) {
super();
this.disposer.add_all(
ctrl.textures.observe(({ value: textures }) => {
this.render_textures(textures);
this.schedule_render();
}),
);
}
dispose(): void {
super.dispose();
this.disposer.dispose();
}
private render_textures(textures: readonly XvrTexture[]): void {
this.scene.delete();
let total_width = 10 * (textures.length - 1); // 10px spacing between textures.
let total_height = 0;
for (const tex of textures) {
total_width += tex.width;
total_height = Math.max(total_height, tex.height);
}
let x = -Math.floor(total_width / 2);
const y = -Math.floor(total_height / 2);
for (const tex of textures) {
try {
const quad_mesh = this.create_quad(tex);
this.scene.add_child(
this.scene.root_node,
quad_mesh,
new TranslateTransform(x, y + (total_height - tex.height) / 2, 0),
);
} catch (e) {
logger.error("Couldn't create quad for texture.", e);
}
x += 10 + tex.width;
}
}
private create_quad(tex: XvrTexture): Mesh {
return new MeshBuilder(VertexFormat.PosTex)
.vertex(0, 0, 0, 0, 1)
.vertex(tex.width, 0, 0, 1, 1)
.vertex(tex.width, tex.height, 0, 1, 0)
.vertex(0, tex.height, 0, 0, 0)
.triangle(0, 1, 2)
.triangle(2, 3, 0)
.texture(xvr_texture_to_texture(tex))
.build();
}
}
export function xvr_texture_to_texture(tex: XvrTexture): Texture {
let format: TextureFormat;
let data_size: number;
// Ignore mipmaps.
switch (tex.format[1]) {
case 6:
format = TextureFormat.RGBA_S3TC_DXT1;
data_size = (tex.width * tex.height) / 2;
break;
case 7:
format = TextureFormat.RGBA_S3TC_DXT3;
data_size = tex.width * tex.height;
break;
default:
throw new Error(`Format ${tex.format.join(", ")} not supported.`);
}
return new Texture(tex.width, tex.height, format, tex.data.slice(0, data_size));
}

View File

@ -1,4 +1,4 @@
import { DisposableThreeRenderer } from "../../../../src/core/rendering/Renderer";
import { DisposableThreeRenderer } from "../../../../src/core/rendering/ThreeRenderer";
export class StubThreeRenderer implements DisposableThreeRenderer {
domElement: HTMLCanvasElement = document.createElement("canvas");