|
|
|
@@ -1,28 +1,44 @@
|
|
|
|
|
import { Camera } from "../components/camera.ts";
|
|
|
|
|
import { mat4 } from "gl-matrix";
|
|
|
|
|
|
|
|
|
|
export let gl: WebGLRenderingContext;
|
|
|
|
|
export let device: GPUDevice;
|
|
|
|
|
export let canvas: HTMLCanvasElement;
|
|
|
|
|
|
|
|
|
|
const MAX_SPRITES = 100000;
|
|
|
|
|
const VERTS_PER_SPRITE = 6;
|
|
|
|
|
const FLOATS_PER_VERT = 9;
|
|
|
|
|
const VERTEX_STRIDE = FLOATS_PER_VERT * 4;
|
|
|
|
|
|
|
|
|
|
let program: WebGLProgram;
|
|
|
|
|
let main_buffer: WebGLBuffer;
|
|
|
|
|
const SAMPLE_COUNT = 4;
|
|
|
|
|
const DEPTH_FORMAT: GPUTextureFormat = "depth24plus";
|
|
|
|
|
|
|
|
|
|
// dynamic uniform offsets have to be 256 aligned
|
|
|
|
|
const UNIFORM_SLOT_SIZE = 256;
|
|
|
|
|
|
|
|
|
|
let context: GPUCanvasContext;
|
|
|
|
|
let canvas_format: GPUTextureFormat;
|
|
|
|
|
|
|
|
|
|
let pipeline_2d: GPURenderPipeline;
|
|
|
|
|
let pipeline_3d: GPURenderPipeline;
|
|
|
|
|
let uniform_layout: GPUBindGroupLayout;
|
|
|
|
|
let texture_layout: GPUBindGroupLayout;
|
|
|
|
|
let sampler: GPUSampler;
|
|
|
|
|
|
|
|
|
|
const vertex_data = new Float32Array(MAX_SPRITES * VERTS_PER_SPRITE * FLOATS_PER_VERT);
|
|
|
|
|
let vert_index = 0;
|
|
|
|
|
|
|
|
|
|
let pos_loc: GLint;
|
|
|
|
|
let uv_loc: GLint;
|
|
|
|
|
let color_loc: GLint;
|
|
|
|
|
let texture_loc: WebGLUniformLocation | null;
|
|
|
|
|
let mvp_loc: WebGLUniformLocation | null;
|
|
|
|
|
let col_diffuse_loc: WebGLUniformLocation | null;
|
|
|
|
|
// every flush in a frame appends to this, so earlier draws dont get overwritten before the submit
|
|
|
|
|
let stream_buffer: GPUBuffer;
|
|
|
|
|
let stream_offset = 0;
|
|
|
|
|
|
|
|
|
|
let current_texture: WebGLTexture | null = null;
|
|
|
|
|
export let white_tex: WebGLTexture | null = null;
|
|
|
|
|
let uniform_buffer: GPUBuffer;
|
|
|
|
|
let uniform_bind_group: GPUBindGroup;
|
|
|
|
|
let uniform_slot = -1;
|
|
|
|
|
|
|
|
|
|
const texture_bind_groups = new WeakMap<GPUTexture, GPUBindGroup>();
|
|
|
|
|
|
|
|
|
|
let current_texture: GPUTexture | null = null;
|
|
|
|
|
export let white_tex: GPUTexture | null = null;
|
|
|
|
|
|
|
|
|
|
let mode3d = false;
|
|
|
|
|
|
|
|
|
@@ -33,91 +49,124 @@ const proj = mat4.create();
|
|
|
|
|
const view = mat4.create();
|
|
|
|
|
const mvp = mat4.create();
|
|
|
|
|
|
|
|
|
|
const vertex_src = `#version 300 es
|
|
|
|
|
precision mediump float;
|
|
|
|
|
in vec3 vertexPosition;
|
|
|
|
|
in vec2 vertexTexCoord;
|
|
|
|
|
in vec4 vertexColor;
|
|
|
|
|
out vec2 fragTexCoord;
|
|
|
|
|
out vec4 fragColor;
|
|
|
|
|
uniform mat4 mvp;
|
|
|
|
|
// per frame state
|
|
|
|
|
let encoder: GPUCommandEncoder | undefined;
|
|
|
|
|
let pass: GPURenderPassEncoder | undefined;
|
|
|
|
|
let frame_view: GPUTextureView;
|
|
|
|
|
let msaa_texture: GPUTexture | undefined;
|
|
|
|
|
let depth_texture: GPUTexture | undefined;
|
|
|
|
|
let current_pipeline: GPURenderPipeline | undefined;
|
|
|
|
|
let clear_color: GPUColor = { r: 0, g: 0, b: 0, a: 1 };
|
|
|
|
|
let pending_color_clear = false;
|
|
|
|
|
let pending_depth_clear = false;
|
|
|
|
|
let scissor: { x: number; y: number; width: number; height: number } | undefined;
|
|
|
|
|
let pending_destroy: GPUBuffer[] = [];
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
fragTexCoord = vertexTexCoord;
|
|
|
|
|
fragColor = vertexColor;
|
|
|
|
|
gl_Position = mvp*vec4(vertexPosition, 1.0);
|
|
|
|
|
const shader_src = /* wgsl */ `
|
|
|
|
|
struct Uniforms {
|
|
|
|
|
mvp: mat4x4<f32>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
|
|
|
@group(1) @binding(0) var texture0: texture_2d<f32>;
|
|
|
|
|
@group(1) @binding(1) var sampler0: sampler;
|
|
|
|
|
|
|
|
|
|
struct VertexOut {
|
|
|
|
|
@builtin(position) position: vec4<f32>,
|
|
|
|
|
@location(0) tex_coord: vec2<f32>,
|
|
|
|
|
@location(1) color: vec4<f32>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@vertex
|
|
|
|
|
fn vs_main(
|
|
|
|
|
@location(0) position: vec3<f32>,
|
|
|
|
|
@location(1) tex_coord: vec2<f32>,
|
|
|
|
|
@location(2) color: vec4<f32>,
|
|
|
|
|
) -> VertexOut {
|
|
|
|
|
var out: VertexOut;
|
|
|
|
|
out.position = uniforms.mvp * vec4<f32>(position, 1.0);
|
|
|
|
|
out.tex_coord = tex_coord;
|
|
|
|
|
out.color = color;
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@fragment
|
|
|
|
|
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
|
|
|
|
|
return textureSample(texture0, sampler0, in.tex_coord) * in.color;
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const fragment_src = `#version 300 es
|
|
|
|
|
precision mediump float;
|
|
|
|
|
in vec2 fragTexCoord;
|
|
|
|
|
in vec4 fragColor;
|
|
|
|
|
out vec4 finalColor;
|
|
|
|
|
uniform sampler2D texture0;
|
|
|
|
|
uniform vec4 colDiffuse;
|
|
|
|
|
void main() {
|
|
|
|
|
vec4 texelColor = texture(texture0, fragTexCoord);
|
|
|
|
|
finalColor = texelColor*colDiffuse*fragColor;
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
export function init_window(canvas_element: HTMLCanvasElement) {
|
|
|
|
|
export async function init_window(canvas_element: HTMLCanvasElement) {
|
|
|
|
|
canvas = canvas_element;
|
|
|
|
|
canvas.width = 1800;
|
|
|
|
|
canvas.height = 900;
|
|
|
|
|
|
|
|
|
|
const ctx = canvas.getContext("webgl2", { antialias: true, alpha: false });
|
|
|
|
|
if (!ctx) {
|
|
|
|
|
throw new Error("WebGL2 not supported");
|
|
|
|
|
if (!navigator.gpu) {
|
|
|
|
|
throw new Error("WebGPU not supported");
|
|
|
|
|
}
|
|
|
|
|
const adapter = await navigator.gpu.requestAdapter();
|
|
|
|
|
if (!adapter) {
|
|
|
|
|
throw new Error("No WebGPU adapter found");
|
|
|
|
|
}
|
|
|
|
|
device = await adapter.requestDevice();
|
|
|
|
|
device.addEventListener("uncapturederror", (event) => {
|
|
|
|
|
console.error("WebGPU error:", (event as GPUUncapturedErrorEvent).error.message);
|
|
|
|
|
});
|
|
|
|
|
device.lost.then((info) => console.error(`WebGPU device lost: ${info.message}`));
|
|
|
|
|
|
|
|
|
|
gl = ctx;
|
|
|
|
|
// the dom typings dont know about the webgpu overload yet
|
|
|
|
|
const ctx = canvas.getContext("webgpu") as GPUCanvasContext | null;
|
|
|
|
|
if (!ctx) {
|
|
|
|
|
throw new Error("WebGPU not supported");
|
|
|
|
|
}
|
|
|
|
|
context = ctx;
|
|
|
|
|
canvas_format = navigator.gpu.getPreferredCanvasFormat();
|
|
|
|
|
context.configure({ device, format: canvas_format, alphaMode: "opaque" });
|
|
|
|
|
|
|
|
|
|
program = create_program(vertex_src, fragment_src);
|
|
|
|
|
create_pipelines();
|
|
|
|
|
|
|
|
|
|
main_buffer = gl.createBuffer()!;
|
|
|
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, main_buffer);
|
|
|
|
|
sampler = device.createSampler({
|
|
|
|
|
magFilter: "nearest",
|
|
|
|
|
minFilter: "nearest",
|
|
|
|
|
addressModeU: "clamp-to-edge",
|
|
|
|
|
addressModeV: "clamp-to-edge",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const stride = FLOATS_PER_VERT * 4;
|
|
|
|
|
|
|
|
|
|
pos_loc = gl.getAttribLocation(program, "vertexPosition");
|
|
|
|
|
gl.enableVertexAttribArray(pos_loc);
|
|
|
|
|
gl.vertexAttribPointer(pos_loc, 3, gl.FLOAT, false, stride, 0);
|
|
|
|
|
|
|
|
|
|
uv_loc = gl.getAttribLocation(program, "vertexTexCoord");
|
|
|
|
|
gl.enableVertexAttribArray(uv_loc);
|
|
|
|
|
gl.vertexAttribPointer(uv_loc, 2, gl.FLOAT, false, stride, 12);
|
|
|
|
|
|
|
|
|
|
color_loc = gl.getAttribLocation(program, "vertexColor");
|
|
|
|
|
gl.enableVertexAttribArray(color_loc);
|
|
|
|
|
gl.vertexAttribPointer(color_loc, 4, gl.FLOAT, false, stride, 20);
|
|
|
|
|
|
|
|
|
|
mvp_loc = gl.getUniformLocation(program, "mvp");
|
|
|
|
|
texture_loc = gl.getUniformLocation(program, "texture0");
|
|
|
|
|
col_diffuse_loc = gl.getUniformLocation(program, "colDiffuse");
|
|
|
|
|
|
|
|
|
|
gl.useProgram(program);
|
|
|
|
|
|
|
|
|
|
gl.enable(gl.BLEND);
|
|
|
|
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
|
|
|
|
|
|
|
|
gl.cullFace(gl.BACK);
|
|
|
|
|
gl.frontFace(gl.CCW);
|
|
|
|
|
stream_buffer = create_stream_buffer(8 * 1024 * 1024);
|
|
|
|
|
create_uniform_buffer(64);
|
|
|
|
|
|
|
|
|
|
create_white_texture();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function begin_drawing() {
|
|
|
|
|
encoder = device.createCommandEncoder();
|
|
|
|
|
frame_view = context.getCurrentTexture().createView();
|
|
|
|
|
ensure_render_targets();
|
|
|
|
|
|
|
|
|
|
update_2d_mvp();
|
|
|
|
|
gl.depthFunc(gl.LEQUAL);
|
|
|
|
|
gl.clearDepth(1.0);
|
|
|
|
|
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
|
|
|
stream_offset = 0;
|
|
|
|
|
uniform_slot = -1;
|
|
|
|
|
write_mvp();
|
|
|
|
|
|
|
|
|
|
pending_color_clear = true;
|
|
|
|
|
pending_depth_clear = true;
|
|
|
|
|
vert_index = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function end_drawing() {
|
|
|
|
|
flush_batch();
|
|
|
|
|
// make sure the frame gets cleared even if nothing was drawn
|
|
|
|
|
ensure_pass();
|
|
|
|
|
|
|
|
|
|
pass!.end();
|
|
|
|
|
pass = undefined;
|
|
|
|
|
device.queue.submit([encoder!.finish()]);
|
|
|
|
|
encoder = undefined;
|
|
|
|
|
|
|
|
|
|
for (const buffer of pending_destroy) {
|
|
|
|
|
buffer.destroy();
|
|
|
|
|
}
|
|
|
|
|
pending_destroy = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function update_2d_mvp() {
|
|
|
|
@@ -131,19 +180,23 @@ function update_2d_mvp() {
|
|
|
|
|
const n = -1;
|
|
|
|
|
const f = 1;
|
|
|
|
|
|
|
|
|
|
mat4.ortho(ortho, l, r, b, t, n, f);
|
|
|
|
|
mat4.orthoZO(ortho, l, r, b, t, n, f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function begin_clip(x: number, y: number, width: number, height: number) {
|
|
|
|
|
gl.enable(gl.SCISSOR_TEST);
|
|
|
|
|
|
|
|
|
|
const flipped_y = canvas.height - (y + height);
|
|
|
|
|
|
|
|
|
|
gl.scissor(x, flipped_y, width, height);
|
|
|
|
|
flush_batch();
|
|
|
|
|
scissor = { x, y, width, height };
|
|
|
|
|
if (pass) {
|
|
|
|
|
apply_scissor(pass);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function end_clip() {
|
|
|
|
|
gl.disable(gl.SCISSOR_TEST);
|
|
|
|
|
flush_batch();
|
|
|
|
|
scissor = undefined;
|
|
|
|
|
if (pass) {
|
|
|
|
|
apply_scissor(pass);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function begin_mode_3d(new_camera: Camera) {
|
|
|
|
@@ -152,13 +205,8 @@ export function begin_mode_3d(new_camera: Camera) {
|
|
|
|
|
mode3d = true;
|
|
|
|
|
camera = new_camera;
|
|
|
|
|
|
|
|
|
|
gl.enable(gl.DEPTH_TEST);
|
|
|
|
|
gl.enable(gl.CULL_FACE);
|
|
|
|
|
gl.depthMask(true);
|
|
|
|
|
gl.enable(gl.POLYGON_OFFSET_FILL);
|
|
|
|
|
gl.polygonOffset(1, 1);
|
|
|
|
|
|
|
|
|
|
update_camera();
|
|
|
|
|
write_mvp();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function end_mode_3d() {
|
|
|
|
@@ -169,15 +217,18 @@ export function end_mode_3d() {
|
|
|
|
|
flush_batch();
|
|
|
|
|
|
|
|
|
|
mode3d = false;
|
|
|
|
|
gl.disable(gl.DEPTH_TEST);
|
|
|
|
|
gl.disable(gl.CULL_FACE);
|
|
|
|
|
gl.depthMask(false);
|
|
|
|
|
gl.disable(gl.POLYGON_OFFSET_FILL);
|
|
|
|
|
write_mvp();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function clear_background(r: number, g: number, b: number, a = 1) {
|
|
|
|
|
gl.clearColor(r, g, b, a);
|
|
|
|
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
|
|
|
flush_batch();
|
|
|
|
|
clear_color = { r, g, b, a };
|
|
|
|
|
// the clear happens when a render pass starts, so start a new one
|
|
|
|
|
if (pass) {
|
|
|
|
|
pass.end();
|
|
|
|
|
pass = undefined;
|
|
|
|
|
}
|
|
|
|
|
pending_color_clear = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function flush_batch() {
|
|
|
|
@@ -185,52 +236,41 @@ export function flush_batch() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, main_buffer);
|
|
|
|
|
gl.bufferData(gl.ARRAY_BUFFER, vertex_data.subarray(0, vert_index), gl.STREAM_DRAW);
|
|
|
|
|
|
|
|
|
|
if (mode3d) {
|
|
|
|
|
gl.uniformMatrix4fv(mvp_loc, false, mvp);
|
|
|
|
|
} else {
|
|
|
|
|
gl.uniformMatrix4fv(mvp_loc, false, ortho);
|
|
|
|
|
const byte_length = vert_index * 4;
|
|
|
|
|
if (stream_offset + byte_length > stream_buffer.size) {
|
|
|
|
|
// the old buffer might still be used by draws in this frame, destroy it after the submit
|
|
|
|
|
pending_destroy.push(stream_buffer);
|
|
|
|
|
stream_buffer = create_stream_buffer(Math.max(stream_buffer.size * 2, byte_length));
|
|
|
|
|
stream_offset = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const stride = FLOATS_PER_VERT * 4;
|
|
|
|
|
gl.vertexAttribPointer(pos_loc, 3, gl.FLOAT, false, stride, 0);
|
|
|
|
|
gl.vertexAttribPointer(uv_loc, 2, gl.FLOAT, false, stride, 12);
|
|
|
|
|
gl.vertexAttribPointer(color_loc, 4, gl.FLOAT, false, stride, 20);
|
|
|
|
|
|
|
|
|
|
gl.uniform4f(col_diffuse_loc, 1, 1, 1, 1);
|
|
|
|
|
|
|
|
|
|
gl.activeTexture(gl.TEXTURE0);
|
|
|
|
|
gl.bindTexture(gl.TEXTURE_2D, current_texture);
|
|
|
|
|
gl.uniform1i(texture_loc, 0);
|
|
|
|
|
|
|
|
|
|
gl.drawArrays(gl.TRIANGLES, 0, vert_index / FLOATS_PER_VERT);
|
|
|
|
|
device.queue.writeBuffer(stream_buffer, stream_offset, vertex_data, 0, vert_index);
|
|
|
|
|
draw(stream_buffer, stream_offset, vert_index / FLOATS_PER_VERT);
|
|
|
|
|
stream_offset += byte_length;
|
|
|
|
|
|
|
|
|
|
vert_index = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function flush_buffer(buffer: WebGLBuffer, draw_count: number) {
|
|
|
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
|
|
|
|
|
|
|
|
if (mode3d) {
|
|
|
|
|
gl.uniformMatrix4fv(mvp_loc, false, mvp);
|
|
|
|
|
} else {
|
|
|
|
|
gl.uniformMatrix4fv(mvp_loc, false, ortho);
|
|
|
|
|
export function flush_buffer(buffer: GPUBuffer, draw_count: number) {
|
|
|
|
|
draw(buffer, 0, draw_count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const stride = FLOATS_PER_VERT * 4;
|
|
|
|
|
gl.vertexAttribPointer(pos_loc, 3, gl.FLOAT, false, stride, 0);
|
|
|
|
|
gl.vertexAttribPointer(uv_loc, 2, gl.FLOAT, false, stride, 12);
|
|
|
|
|
gl.vertexAttribPointer(color_loc, 4, gl.FLOAT, false, stride, 20);
|
|
|
|
|
export function create_vertex_buffer(vertices: Float32Array): GPUBuffer {
|
|
|
|
|
const buffer = device.createBuffer({
|
|
|
|
|
size: Math.max(4, vertices.byteLength),
|
|
|
|
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
|
|
|
});
|
|
|
|
|
device.queue.writeBuffer(buffer, 0, vertices);
|
|
|
|
|
return buffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gl.uniform4f(col_diffuse_loc, 1, 1, 1, 1);
|
|
|
|
|
|
|
|
|
|
gl.activeTexture(gl.TEXTURE0);
|
|
|
|
|
gl.bindTexture(gl.TEXTURE_2D, current_texture);
|
|
|
|
|
gl.uniform1i(texture_loc, 0);
|
|
|
|
|
|
|
|
|
|
gl.drawArrays(gl.TRIANGLES, 0, draw_count);
|
|
|
|
|
export function destroy_vertex_buffer(buffer: GPUBuffer) {
|
|
|
|
|
// it might be used by a draw thats not submitted yet
|
|
|
|
|
if (encoder) {
|
|
|
|
|
pending_destroy.push(buffer);
|
|
|
|
|
} else {
|
|
|
|
|
buffer.destroy();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resize_canvas() {
|
|
|
|
@@ -242,21 +282,26 @@ export function resize_canvas() {
|
|
|
|
|
canvas.height = height;
|
|
|
|
|
canvas.style.width = canvas.width + "px";
|
|
|
|
|
canvas.style.height = canvas.height + "px";
|
|
|
|
|
|
|
|
|
|
gl.viewport(0, 0, width, height);
|
|
|
|
|
|
|
|
|
|
gl.useProgram(program);
|
|
|
|
|
// render targets get recreated at the start of the next frame
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function get_current_texture(): WebGLTexture | null {
|
|
|
|
|
export function get_current_texture(): GPUTexture | null {
|
|
|
|
|
return current_texture;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function set_current_texture(texture: WebGLTexture) {
|
|
|
|
|
export function set_current_texture(texture: GPUTexture) {
|
|
|
|
|
current_texture = texture;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function create_texture(width: number, height: number): GPUTexture {
|
|
|
|
|
return device.createTexture({
|
|
|
|
|
size: [width, height],
|
|
|
|
|
format: "rgba8unorm",
|
|
|
|
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function push_vertex(px: number, py: number, pz: number, u: number, vv: number, r = 1, g = 1, b = 1, a = 1) {
|
|
|
|
|
let i = vert_index;
|
|
|
|
|
vertex_data[i++] = px;
|
|
|
|
@@ -330,12 +375,115 @@ export function push_quad_vertices(
|
|
|
|
|
|
|
|
|
|
// internal
|
|
|
|
|
|
|
|
|
|
function draw(buffer: GPUBuffer, offset: number, vertex_count: number) {
|
|
|
|
|
if (!current_texture || vertex_count === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const render_pass = ensure_pass();
|
|
|
|
|
|
|
|
|
|
const pipeline = mode3d ? pipeline_3d : pipeline_2d;
|
|
|
|
|
if (current_pipeline !== pipeline) {
|
|
|
|
|
render_pass.setPipeline(pipeline);
|
|
|
|
|
current_pipeline = pipeline;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render_pass.setBindGroup(0, uniform_bind_group, [uniform_slot * UNIFORM_SLOT_SIZE]);
|
|
|
|
|
render_pass.setBindGroup(1, get_texture_bind_group(current_texture));
|
|
|
|
|
render_pass.setVertexBuffer(0, buffer, offset);
|
|
|
|
|
render_pass.draw(vertex_count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ensure_pass(): GPURenderPassEncoder {
|
|
|
|
|
if (pass) {
|
|
|
|
|
return pass;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pass = encoder!.beginRenderPass({
|
|
|
|
|
colorAttachments: [{
|
|
|
|
|
view: msaa_texture!.createView(),
|
|
|
|
|
resolveTarget: frame_view,
|
|
|
|
|
clearValue: clear_color,
|
|
|
|
|
loadOp: pending_color_clear ? "clear" : "load",
|
|
|
|
|
storeOp: "store",
|
|
|
|
|
}],
|
|
|
|
|
depthStencilAttachment: {
|
|
|
|
|
view: depth_texture!.createView(),
|
|
|
|
|
depthClearValue: 1,
|
|
|
|
|
depthLoadOp: pending_depth_clear ? "clear" : "load",
|
|
|
|
|
depthStoreOp: "store",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
pending_color_clear = false;
|
|
|
|
|
pending_depth_clear = false;
|
|
|
|
|
current_pipeline = undefined;
|
|
|
|
|
apply_scissor(pass);
|
|
|
|
|
|
|
|
|
|
return pass;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function apply_scissor(render_pass: GPURenderPassEncoder) {
|
|
|
|
|
const target_width = msaa_texture!.width;
|
|
|
|
|
const target_height = msaa_texture!.height;
|
|
|
|
|
|
|
|
|
|
if (!scissor) {
|
|
|
|
|
render_pass.setScissorRect(0, 0, target_width, target_height);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// webgpu errors on rects outside the target instead of clipping them
|
|
|
|
|
const x0 = Math.min(target_width, Math.max(0, Math.floor(scissor.x)));
|
|
|
|
|
const y0 = Math.min(target_height, Math.max(0, Math.floor(scissor.y)));
|
|
|
|
|
const x1 = Math.min(target_width, Math.max(x0, Math.ceil(scissor.x + scissor.width)));
|
|
|
|
|
const y1 = Math.min(target_height, Math.max(y0, Math.ceil(scissor.y + scissor.height)));
|
|
|
|
|
|
|
|
|
|
render_pass.setScissorRect(x0, y0, x1 - x0, y1 - y0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ensure_render_targets() {
|
|
|
|
|
const width = canvas.width;
|
|
|
|
|
const height = canvas.height;
|
|
|
|
|
|
|
|
|
|
if (msaa_texture && msaa_texture.width === width && msaa_texture.height === height) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
msaa_texture?.destroy();
|
|
|
|
|
depth_texture?.destroy();
|
|
|
|
|
|
|
|
|
|
msaa_texture = device.createTexture({
|
|
|
|
|
size: [width, height],
|
|
|
|
|
format: canvas_format,
|
|
|
|
|
sampleCount: SAMPLE_COUNT,
|
|
|
|
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
|
|
|
});
|
|
|
|
|
depth_texture = device.createTexture({
|
|
|
|
|
size: [width, height],
|
|
|
|
|
format: DEPTH_FORMAT,
|
|
|
|
|
sampleCount: SAMPLE_COUNT,
|
|
|
|
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// puts the current matrix in a new uniform slot, earlier draws in the frame keep using theirs
|
|
|
|
|
function write_mvp() {
|
|
|
|
|
uniform_slot += 1;
|
|
|
|
|
if (uniform_slot * UNIFORM_SLOT_SIZE >= uniform_buffer.size) {
|
|
|
|
|
pending_destroy.push(uniform_buffer);
|
|
|
|
|
create_uniform_buffer((uniform_buffer.size / UNIFORM_SLOT_SIZE) * 2);
|
|
|
|
|
uniform_slot = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const matrix = mode3d ? mvp : ortho;
|
|
|
|
|
device.queue.writeBuffer(uniform_buffer, uniform_slot * UNIFORM_SLOT_SIZE, matrix as Float32Array);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function update_camera() {
|
|
|
|
|
if (!camera) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mat4.perspective(
|
|
|
|
|
mat4.perspectiveZO(
|
|
|
|
|
proj,
|
|
|
|
|
camera.fov,
|
|
|
|
|
canvas.width / canvas.height,
|
|
|
|
@@ -354,47 +502,107 @@ function update_camera() {
|
|
|
|
|
mat4.multiply(mvp, proj, view);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function compile_shader(type: number, src: string) {
|
|
|
|
|
const shader = gl.createShader(type)!;
|
|
|
|
|
function create_pipelines() {
|
|
|
|
|
const module = device.createShaderModule({ code: shader_src });
|
|
|
|
|
|
|
|
|
|
gl.shaderSource(shader, src);
|
|
|
|
|
gl.compileShader(shader);
|
|
|
|
|
uniform_layout = device.createBindGroupLayout({
|
|
|
|
|
entries: [{
|
|
|
|
|
binding: 0,
|
|
|
|
|
visibility: GPUShaderStage.VERTEX,
|
|
|
|
|
buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 64 },
|
|
|
|
|
}],
|
|
|
|
|
});
|
|
|
|
|
texture_layout = device.createBindGroupLayout({
|
|
|
|
|
entries: [
|
|
|
|
|
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: {} },
|
|
|
|
|
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
const layout = device.createPipelineLayout({ bindGroupLayouts: [uniform_layout, texture_layout] });
|
|
|
|
|
|
|
|
|
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
|
|
|
console.error(gl.getShaderInfoLog(shader));
|
|
|
|
|
throw new Error("Shader compile failed");
|
|
|
|
|
const vertex: GPUVertexState = {
|
|
|
|
|
module,
|
|
|
|
|
entryPoint: "vs_main",
|
|
|
|
|
buffers: [{
|
|
|
|
|
arrayStride: VERTEX_STRIDE,
|
|
|
|
|
attributes: [
|
|
|
|
|
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
|
|
|
{ shaderLocation: 1, offset: 12, format: "float32x2" },
|
|
|
|
|
{ shaderLocation: 2, offset: 20, format: "float32x4" },
|
|
|
|
|
],
|
|
|
|
|
}],
|
|
|
|
|
};
|
|
|
|
|
const fragment: GPUFragmentState = {
|
|
|
|
|
module,
|
|
|
|
|
entryPoint: "fs_main",
|
|
|
|
|
targets: [{
|
|
|
|
|
format: canvas_format,
|
|
|
|
|
blend: {
|
|
|
|
|
color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha", operation: "add" },
|
|
|
|
|
alpha: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha", operation: "add" },
|
|
|
|
|
},
|
|
|
|
|
}],
|
|
|
|
|
};
|
|
|
|
|
const multisample: GPUMultisampleState = { count: SAMPLE_COUNT };
|
|
|
|
|
|
|
|
|
|
pipeline_2d = device.createRenderPipeline({
|
|
|
|
|
layout,
|
|
|
|
|
vertex,
|
|
|
|
|
fragment,
|
|
|
|
|
multisample,
|
|
|
|
|
primitive: { topology: "triangle-list", cullMode: "none" },
|
|
|
|
|
depthStencil: { format: DEPTH_FORMAT, depthWriteEnabled: false, depthCompare: "always" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
pipeline_3d = device.createRenderPipeline({
|
|
|
|
|
layout,
|
|
|
|
|
vertex,
|
|
|
|
|
fragment,
|
|
|
|
|
multisample,
|
|
|
|
|
primitive: { topology: "triangle-list", cullMode: "back", frontFace: "ccw" },
|
|
|
|
|
depthStencil: {
|
|
|
|
|
format: DEPTH_FORMAT,
|
|
|
|
|
depthWriteEnabled: true,
|
|
|
|
|
depthCompare: "less-equal",
|
|
|
|
|
// same as the old polygonOffset(1, 1)
|
|
|
|
|
depthBias: 1,
|
|
|
|
|
depthBiasSlopeScale: 1,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return shader;
|
|
|
|
|
function create_stream_buffer(size: number) {
|
|
|
|
|
return device.createBuffer({ size, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function create_program(vs_src: string, fs_src: string) {
|
|
|
|
|
const vs = compile_shader(gl.VERTEX_SHADER, vs_src);
|
|
|
|
|
const fs = compile_shader(gl.FRAGMENT_SHADER, fs_src);
|
|
|
|
|
|
|
|
|
|
const program = gl.createProgram()!;
|
|
|
|
|
gl.attachShader(program, vs);
|
|
|
|
|
gl.attachShader(program, fs);
|
|
|
|
|
gl.linkProgram(program);
|
|
|
|
|
|
|
|
|
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
|
|
|
console.error(gl.getProgramInfoLog(program));
|
|
|
|
|
throw new Error("Program link failed");
|
|
|
|
|
function create_uniform_buffer(slots: number) {
|
|
|
|
|
uniform_buffer = device.createBuffer({
|
|
|
|
|
size: slots * UNIFORM_SLOT_SIZE,
|
|
|
|
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
|
|
|
});
|
|
|
|
|
uniform_bind_group = device.createBindGroup({
|
|
|
|
|
layout: uniform_layout,
|
|
|
|
|
entries: [{ binding: 0, resource: { buffer: uniform_buffer, size: 64 } }],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return program;
|
|
|
|
|
function get_texture_bind_group(texture: GPUTexture) {
|
|
|
|
|
let bind_group = texture_bind_groups.get(texture);
|
|
|
|
|
if (!bind_group) {
|
|
|
|
|
bind_group = device.createBindGroup({
|
|
|
|
|
layout: texture_layout,
|
|
|
|
|
entries: [
|
|
|
|
|
{ binding: 0, resource: texture.createView() },
|
|
|
|
|
{ binding: 1, resource: sampler },
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
texture_bind_groups.set(texture, bind_group);
|
|
|
|
|
}
|
|
|
|
|
return bind_group;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function create_white_texture() {
|
|
|
|
|
const tex = gl.createTexture();
|
|
|
|
|
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
|
|
|
const white_pixel = new Uint8Array([255, 255, 255, 255]);
|
|
|
|
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, white_pixel);
|
|
|
|
|
|
|
|
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
|
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
const tex = create_texture(1, 1);
|
|
|
|
|
device.queue.writeTexture({ texture: tex }, new Uint8Array([255, 255, 255, 255]), { bytesPerRow: 4 }, [1, 1]);
|
|
|
|
|
white_tex = tex;
|
|
|
|
|
}
|
|
|
|
|