mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58: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 { 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";
|
||||
|
@ -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" });
|
||||
|
@ -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";
|
||||
|
||||
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;
|
||||
}
|
||||
|
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 { 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";
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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(
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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[] = [];
|
||||
|
||||
|
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 {
|
||||
domElement: HTMLCanvasElement = document.createElement("canvas");
|
||||
|
Loading…
Reference in New Issue
Block a user