mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-05 15:28:29 +08:00
Added experimental WebGL renderer.
This commit is contained in:
parent
f4d9cb290e
commit
85ccdbb0a6
@ -5,7 +5,7 @@ import { create_item_type_stores } from "../core/stores/ItemTypeStore";
|
|||||||
import { create_item_drop_stores } from "../hunt_optimizer/stores/ItemDropStore";
|
import { create_item_drop_stores } from "../hunt_optimizer/stores/ItemDropStore";
|
||||||
import { ApplicationView } from "./gui/ApplicationView";
|
import { ApplicationView } from "./gui/ApplicationView";
|
||||||
import { throttle } from "lodash";
|
import { throttle } from "lodash";
|
||||||
import { DisposableThreeRenderer } from "../core/rendering/Renderer";
|
import { DisposableThreeRenderer } from "../core/rendering/ThreeRenderer";
|
||||||
import { Disposer } from "../core/observable/Disposer";
|
import { Disposer } from "../core/observable/Disposer";
|
||||||
import { disposable_custom_listener, disposable_listener } from "../core/gui/dom";
|
import { disposable_custom_listener, disposable_listener } from "../core/gui/dom";
|
||||||
import { Random } from "../core/Random";
|
import { Random } from "../core/Random";
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ResizableWidget } from "./ResizableWidget";
|
import { ResizableWidget } from "./ResizableWidget";
|
||||||
import { Renderer } from "../rendering/Renderer";
|
|
||||||
import { div } from "./dom";
|
import { div } from "./dom";
|
||||||
import { Widget } from "./Widget";
|
import { Widget } from "./Widget";
|
||||||
|
import { Renderer } from "../rendering/Renderer";
|
||||||
|
|
||||||
export class RendererWidget extends ResizableWidget {
|
export class RendererWidget extends ResizableWidget {
|
||||||
readonly element = div({ className: "core_RendererWidget" });
|
readonly element = div({ className: "core_RendererWidget" });
|
||||||
|
@ -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
60
src/core/math/index.ts
Normal 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
162
src/core/rendering/Mesh.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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";
|
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 {
|
export abstract class Renderer implements Disposable {
|
||||||
private _debug = false;
|
abstract readonly canvas_element: HTMLCanvasElement;
|
||||||
|
|
||||||
get debug(): boolean {
|
abstract dispose(): void;
|
||||||
return this._debug;
|
|
||||||
}
|
|
||||||
|
|
||||||
set debug(debug: boolean) {
|
abstract start_rendering(): void;
|
||||||
this._debug = debug;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract readonly camera: PerspectiveCamera | OrthographicCamera;
|
abstract stop_rendering(): void;
|
||||||
readonly controls!: CameraControls;
|
|
||||||
readonly scene = new Scene();
|
|
||||||
readonly light_holder = new Group();
|
|
||||||
|
|
||||||
private readonly renderer: DisposableThreeRenderer;
|
abstract set_size(width: number, height: number): void;
|
||||||
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);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
54
src/core/rendering/Scene.ts
Normal file
54
src/core/rendering/Scene.ts
Normal 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) {}
|
||||||
|
}
|
91
src/core/rendering/ShaderProgram.ts
Normal file
91
src/core/rendering/ShaderProgram.ts
Normal 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;
|
||||||
|
}
|
72
src/core/rendering/Texture.ts
Normal file
72
src/core/rendering/Texture.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
142
src/core/rendering/ThreeRenderer.ts
Normal file
142
src/core/rendering/ThreeRenderer.ts
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
33
src/core/rendering/Transform.ts
Normal file
33
src/core/rendering/Transform.ts
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
27
src/core/rendering/VertexFormat.ts
Normal file
27
src/core/rendering/VertexFormat.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
119
src/core/rendering/WebglRenderer.ts
Normal file
119
src/core/rendering/WebglRenderer.ts
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
55
src/core/rendering/shader_sources.ts
Normal file
55
src/core/rendering/shader_sources.ts
Normal 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);
|
||||||
|
}
|
||||||
|
`;
|
@ -9,7 +9,7 @@ import "@fortawesome/fontawesome-free/js/brands";
|
|||||||
import { initialize_application } from "./application";
|
import { initialize_application } from "./application";
|
||||||
import { FetchClient } from "./core/HttpClient";
|
import { FetchClient } from "./core/HttpClient";
|
||||||
import { WebGLRenderer } from "three";
|
import { WebGLRenderer } from "three";
|
||||||
import { DisposableThreeRenderer } from "./core/rendering/Renderer";
|
import { DisposableThreeRenderer } from "./core/rendering/ThreeRenderer";
|
||||||
import { Random } from "./core/Random";
|
import { Random } from "./core/Random";
|
||||||
import { DateClock } from "./core/Clock";
|
import { DateClock } from "./core/Clock";
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { QuestRendererView } from "./QuestRendererView";
|
|||||||
import { QuestEntityControls } from "../rendering/QuestEntityControls";
|
import { QuestEntityControls } from "../rendering/QuestEntityControls";
|
||||||
import { AreaAssetLoader } from "../loading/AreaAssetLoader";
|
import { AreaAssetLoader } from "../loading/AreaAssetLoader";
|
||||||
import { EntityAssetLoader } from "../loading/EntityAssetLoader";
|
import { EntityAssetLoader } from "../loading/EntityAssetLoader";
|
||||||
import { DisposableThreeRenderer } from "../../core/rendering/Renderer";
|
import { DisposableThreeRenderer } from "../../core/rendering/ThreeRenderer";
|
||||||
|
|
||||||
export class QuestEditorRendererView extends QuestRendererView {
|
export class QuestEditorRendererView extends QuestRendererView {
|
||||||
private readonly entity_controls: QuestEntityControls;
|
private readonly entity_controls: QuestEntityControls;
|
||||||
|
@ -4,7 +4,7 @@ import { QuestRendererView } from "./QuestRendererView";
|
|||||||
import { QuestEditorStore } from "../stores/QuestEditorStore";
|
import { QuestEditorStore } from "../stores/QuestEditorStore";
|
||||||
import { AreaAssetLoader } from "../loading/AreaAssetLoader";
|
import { AreaAssetLoader } from "../loading/AreaAssetLoader";
|
||||||
import { EntityAssetLoader } from "../loading/EntityAssetLoader";
|
import { EntityAssetLoader } from "../loading/EntityAssetLoader";
|
||||||
import { DisposableThreeRenderer } from "../../core/rendering/Renderer";
|
import { DisposableThreeRenderer } from "../../core/rendering/ThreeRenderer";
|
||||||
|
|
||||||
export class QuestRunnerRendererView extends QuestRendererView {
|
export class QuestRunnerRendererView extends QuestRendererView {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -7,7 +7,7 @@ import { AreaAssetLoader } from "./loading/AreaAssetLoader";
|
|||||||
import { HttpClient } from "../core/HttpClient";
|
import { HttpClient } from "../core/HttpClient";
|
||||||
import { EntityImageRenderer } from "./rendering/EntityImageRenderer";
|
import { EntityImageRenderer } from "./rendering/EntityImageRenderer";
|
||||||
import { EntityAssetLoader } from "./loading/EntityAssetLoader";
|
import { EntityAssetLoader } from "./loading/EntityAssetLoader";
|
||||||
import { DisposableThreeRenderer } from "../core/rendering/Renderer";
|
import { DisposableThreeRenderer } from "../core/rendering/ThreeRenderer";
|
||||||
import { QuestEditorUiPersister } from "./persistence/QuestEditorUiPersister";
|
import { QuestEditorUiPersister } from "./persistence/QuestEditorUiPersister";
|
||||||
import { QuestEditorToolBarView } from "./gui/QuestEditorToolBarView";
|
import { QuestEditorToolBarView } from "./gui/QuestEditorToolBarView";
|
||||||
import { QuestEditorToolBarController } from "./controllers/QuestEditorToolBarController";
|
import { QuestEditorToolBarController } from "./controllers/QuestEditorToolBarController";
|
||||||
|
@ -4,7 +4,7 @@ import { create_entity_type_mesh } from "./conversion/entities";
|
|||||||
import { sequential } from "../../core/sequential";
|
import { sequential } from "../../core/sequential";
|
||||||
import { EntityAssetLoader } from "../loading/EntityAssetLoader";
|
import { EntityAssetLoader } from "../loading/EntityAssetLoader";
|
||||||
import { Disposable } from "../../core/observable/Disposable";
|
import { Disposable } from "../../core/observable/Disposable";
|
||||||
import { DisposableThreeRenderer } from "../../core/rendering/Renderer";
|
import { DisposableThreeRenderer } from "../../core/rendering/ThreeRenderer";
|
||||||
import { LoadingCache } from "../loading/LoadingCache";
|
import { LoadingCache } from "../loading/LoadingCache";
|
||||||
import { DisposablePromise } from "../../core/DisposablePromise";
|
import { DisposablePromise } from "../../core/DisposablePromise";
|
||||||
|
|
||||||
|
@ -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 { Group, Mesh, MeshLambertMaterial, Object3D, PerspectiveCamera } from "three";
|
||||||
import { QuestEntityModel } from "../model/QuestEntityModel";
|
import { QuestEntityModel } from "../model/QuestEntityModel";
|
||||||
import { Quest3DModelManager } from "./Quest3DModelManager";
|
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 { ColorType, EntityUserData, NPC_COLORS, OBJECT_COLORS } from "./conversion/entities";
|
||||||
import { QuestNpcModel } from "../model/QuestNpcModel";
|
import { QuestNpcModel } from "../model/QuestNpcModel";
|
||||||
|
|
||||||
export class QuestRenderer extends Renderer {
|
export class QuestRenderer extends ThreeRenderer {
|
||||||
private _collision_geometry = new Object3D();
|
private _collision_geometry = new Object3D();
|
||||||
private _render_geometry = new Object3D();
|
private _render_geometry = new Object3D();
|
||||||
private _entity_models = new Object3D();
|
private _entity_models = new Object3D();
|
||||||
|
@ -2,10 +2,10 @@ import { div, Icon } from "../../core/gui/dom";
|
|||||||
import { FileButton } from "../../core/gui/FileButton";
|
import { FileButton } from "../../core/gui/FileButton";
|
||||||
import { ToolBar } from "../../core/gui/ToolBar";
|
import { ToolBar } from "../../core/gui/ToolBar";
|
||||||
import { RendererWidget } from "../../core/gui/RendererWidget";
|
import { RendererWidget } from "../../core/gui/RendererWidget";
|
||||||
import { TextureRenderer } from "../rendering/TextureRenderer";
|
|
||||||
import { ResizableView } from "../../core/gui/ResizableView";
|
import { ResizableView } from "../../core/gui/ResizableView";
|
||||||
import { TextureController } from "../controllers/TextureController";
|
import { TextureController } from "../controllers/TextureController";
|
||||||
import { ResultDialog } from "../../core/gui/ResultDialog";
|
import { ResultDialog } from "../../core/gui/ResultDialog";
|
||||||
|
import { Renderer } from "../../core/rendering/Renderer";
|
||||||
|
|
||||||
export class TextureView extends ResizableView {
|
export class TextureView extends ResizableView {
|
||||||
readonly element = div({ className: "viewer_TextureView" });
|
readonly element = div({ className: "viewer_TextureView" });
|
||||||
@ -20,7 +20,7 @@ export class TextureView extends ResizableView {
|
|||||||
|
|
||||||
private readonly renderer_view: RendererWidget;
|
private readonly renderer_view: RendererWidget;
|
||||||
|
|
||||||
constructor(ctrl: TextureController, renderer: TextureRenderer) {
|
constructor(ctrl: TextureController, renderer: Renderer) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.renderer_view = this.add(new RendererWidget(renderer));
|
this.renderer_view = this.add(new RendererWidget(renderer));
|
||||||
|
@ -1,17 +1,11 @@
|
|||||||
import { ViewerView } from "./gui/ViewerView";
|
import { ViewerView } from "./gui/ViewerView";
|
||||||
import { GuiStore } from "../core/stores/GuiStore";
|
import { GuiStore } from "../core/stores/GuiStore";
|
||||||
import { HttpClient } from "../core/HttpClient";
|
import { HttpClient } from "../core/HttpClient";
|
||||||
import { DisposableThreeRenderer } from "../core/rendering/Renderer";
|
import { DisposableThreeRenderer } from "../core/rendering/ThreeRenderer";
|
||||||
import { Disposable } from "../core/observable/Disposable";
|
import { Disposable } from "../core/observable/Disposable";
|
||||||
import { Disposer } from "../core/observable/Disposer";
|
import { Disposer } from "../core/observable/Disposer";
|
||||||
import { TextureRenderer } from "./rendering/TextureRenderer";
|
|
||||||
import { ModelRenderer } from "./rendering/ModelRenderer";
|
|
||||||
import { Random } from "../core/Random";
|
import { Random } from "../core/Random";
|
||||||
import { ModelToolBarView } from "./gui/model/ModelToolBarView";
|
import { Renderer } from "../core/rendering/Renderer";
|
||||||
import { ModelStore } from "./stores/ModelStore";
|
|
||||||
import { ModelToolBarController } from "./controllers/model/ModelToolBarController";
|
|
||||||
import { CharacterClassOptionsView } from "./gui/model/CharacterClassOptionsView";
|
|
||||||
import { CharacterClassOptionsController } from "./controllers/model/CharacterClassOptionsController";
|
|
||||||
|
|
||||||
export function initialize_viewer(
|
export function initialize_viewer(
|
||||||
http_client: HttpClient,
|
http_client: HttpClient,
|
||||||
@ -26,10 +20,23 @@ export function initialize_viewer(
|
|||||||
|
|
||||||
async () => {
|
async () => {
|
||||||
const { ModelController } = await import("./controllers/model/ModelController");
|
const { ModelController } = await import("./controllers/model/ModelController");
|
||||||
|
const { ModelRenderer } = await import("./rendering/ModelRenderer");
|
||||||
const { ModelView } = await import("./gui/model/ModelView");
|
const { ModelView } = await import("./gui/model/ModelView");
|
||||||
const { CharacterClassAssetLoader } = await import(
|
const { CharacterClassAssetLoader } = await import(
|
||||||
"./loading/CharacterClassAssetLoader"
|
"./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 asset_loader = disposer.add(new CharacterClassAssetLoader(http_client));
|
||||||
const store = disposer.add(new ModelStore(gui_store, asset_loader, random));
|
const store = disposer.add(new ModelStore(gui_store, asset_loader, random));
|
||||||
const model_controller = new ModelController(store);
|
const model_controller = new ModelController(store);
|
||||||
@ -47,12 +54,20 @@ export function initialize_viewer(
|
|||||||
async () => {
|
async () => {
|
||||||
const { TextureController } = await import("./controllers/TextureController");
|
const { TextureController } = await import("./controllers/TextureController");
|
||||||
const { TextureView } = await import("./gui/TextureView");
|
const { TextureView } = await import("./gui/TextureView");
|
||||||
|
|
||||||
const controller = disposer.add(new TextureController());
|
const controller = disposer.add(new TextureController());
|
||||||
|
|
||||||
return new TextureView(
|
let renderer: Renderer;
|
||||||
controller,
|
|
||||||
new TextureRenderer(controller, create_three_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);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ import {
|
|||||||
create_animation_clip,
|
create_animation_clip,
|
||||||
PSO_FRAME_RATE,
|
PSO_FRAME_RATE,
|
||||||
} from "../../core/rendering/conversion/ninja_animation";
|
} 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 { Disposer } from "../../core/observable/Disposer";
|
||||||
import { ChangeEvent } from "../../core/observable/Observable";
|
import { ChangeEvent } from "../../core/observable/Observable";
|
||||||
import { LogManager } from "../../core/Logger";
|
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_POSITION = Object.freeze(new Vector3(0, 10, 20));
|
||||||
const CAMERA_LOOK_AT = Object.freeze(new Vector3(0, 0, 0));
|
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 disposer = new Disposer();
|
||||||
private readonly clock = new Clock();
|
private readonly clock = new Clock();
|
||||||
private character_class_active: boolean;
|
private character_class_active: boolean;
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
Vector3,
|
Vector3,
|
||||||
} from "three";
|
} from "three";
|
||||||
import { Disposable } from "../../core/observable/Disposable";
|
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 { Disposer } from "../../core/observable/Disposer";
|
||||||
import { XvrTexture } from "../../core/data_formats/parsing/ninja/texture";
|
import { XvrTexture } from "../../core/data_formats/parsing/ninja/texture";
|
||||||
import { xvr_texture_to_texture } from "../../core/rendering/conversion/ninja_textures";
|
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");
|
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 disposer = new Disposer();
|
||||||
private readonly quad_meshes: Mesh[] = [];
|
private readonly quad_meshes: Mesh[] = [];
|
||||||
|
|
||||||
|
98
src/viewer/rendering/WebglTextureRenderer.ts
Normal file
98
src/viewer/rendering/WebglTextureRenderer.ts
Normal 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));
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { DisposableThreeRenderer } from "../../../../src/core/rendering/Renderer";
|
import { DisposableThreeRenderer } from "../../../../src/core/rendering/ThreeRenderer";
|
||||||
|
|
||||||
export class StubThreeRenderer implements DisposableThreeRenderer {
|
export class StubThreeRenderer implements DisposableThreeRenderer {
|
||||||
domElement: HTMLCanvasElement = document.createElement("canvas");
|
domElement: HTMLCanvasElement = document.createElement("canvas");
|
||||||
|
Loading…
Reference in New Issue
Block a user