Webgpu renderer
This commit is contained in:
@@ -7,7 +7,7 @@ import { ClientWorld } from "../client_world.ts";
|
|||||||
import { generate_chunk } from "../generation.ts";
|
import { generate_chunk } from "../generation.ts";
|
||||||
import { ItemStack } from "../inventory.ts";
|
import { ItemStack } from "../inventory.ts";
|
||||||
import { PlayerComponent } from "../player.ts";
|
import { PlayerComponent } from "../player.ts";
|
||||||
import { gl, Texture } from "../renderer/mod.ts";
|
import { destroy_vertex_buffer, Texture } from "../renderer/mod.ts";
|
||||||
import { Camera } from "./camera.ts";
|
import { Camera } from "./camera.ts";
|
||||||
|
|
||||||
export interface BlockData<T = unknown> {
|
export interface BlockData<T = unknown> {
|
||||||
@@ -34,9 +34,9 @@ export interface Chunk {
|
|||||||
blocks_data: BlockData[];
|
blocks_data: BlockData[];
|
||||||
generated: boolean;
|
generated: boolean;
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
opaque_vertex_buffer?: WebGLBuffer;
|
opaque_vertex_buffer?: GPUBuffer;
|
||||||
opaque_vertex_count?: number;
|
opaque_vertex_count?: number;
|
||||||
transparent_vertex_buffer?: WebGLBuffer;
|
transparent_vertex_buffer?: GPUBuffer;
|
||||||
transparent_vertex_count?: number;
|
transparent_vertex_count?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,10 +296,12 @@ export class Dimension extends Component {
|
|||||||
|
|
||||||
delete_chunk_mesh(chunk: Chunk) {
|
delete_chunk_mesh(chunk: Chunk) {
|
||||||
if (chunk.opaque_vertex_buffer) {
|
if (chunk.opaque_vertex_buffer) {
|
||||||
gl.deleteBuffer(chunk.opaque_vertex_buffer);
|
destroy_vertex_buffer(chunk.opaque_vertex_buffer);
|
||||||
|
chunk.opaque_vertex_buffer = undefined;
|
||||||
}
|
}
|
||||||
if (chunk.transparent_vertex_buffer) {
|
if (chunk.transparent_vertex_buffer) {
|
||||||
gl.deleteBuffer(chunk.transparent_vertex_buffer);
|
destroy_vertex_buffer(chunk.transparent_vertex_buffer);
|
||||||
|
chunk.transparent_vertex_buffer = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -71,7 +71,7 @@ if (!canvas) {
|
|||||||
throw Error("Canvas was not found");
|
throw Error("Canvas was not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
init_window(canvas);
|
await init_window(canvas);
|
||||||
|
|
||||||
InputManager.initialize(canvas);
|
InputManager.initialize(canvas);
|
||||||
|
|
||||||
|
|||||||
+374
-166
@@ -1,28 +1,44 @@
|
|||||||
import { Camera } from "../components/camera.ts";
|
import { Camera } from "../components/camera.ts";
|
||||||
import { mat4 } from "gl-matrix";
|
import { mat4 } from "gl-matrix";
|
||||||
|
|
||||||
export let gl: WebGLRenderingContext;
|
export let device: GPUDevice;
|
||||||
export let canvas: HTMLCanvasElement;
|
export let canvas: HTMLCanvasElement;
|
||||||
|
|
||||||
const MAX_SPRITES = 100000;
|
const MAX_SPRITES = 100000;
|
||||||
const VERTS_PER_SPRITE = 6;
|
const VERTS_PER_SPRITE = 6;
|
||||||
const FLOATS_PER_VERT = 9;
|
const FLOATS_PER_VERT = 9;
|
||||||
|
const VERTEX_STRIDE = FLOATS_PER_VERT * 4;
|
||||||
|
|
||||||
let program: WebGLProgram;
|
const SAMPLE_COUNT = 4;
|
||||||
let main_buffer: WebGLBuffer;
|
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);
|
const vertex_data = new Float32Array(MAX_SPRITES * VERTS_PER_SPRITE * FLOATS_PER_VERT);
|
||||||
let vert_index = 0;
|
let vert_index = 0;
|
||||||
|
|
||||||
let pos_loc: GLint;
|
// every flush in a frame appends to this, so earlier draws dont get overwritten before the submit
|
||||||
let uv_loc: GLint;
|
let stream_buffer: GPUBuffer;
|
||||||
let color_loc: GLint;
|
let stream_offset = 0;
|
||||||
let texture_loc: WebGLUniformLocation | null;
|
|
||||||
let mvp_loc: WebGLUniformLocation | null;
|
|
||||||
let col_diffuse_loc: WebGLUniformLocation | null;
|
|
||||||
|
|
||||||
let current_texture: WebGLTexture | null = null;
|
let uniform_buffer: GPUBuffer;
|
||||||
export let white_tex: WebGLTexture | null = null;
|
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;
|
let mode3d = false;
|
||||||
|
|
||||||
@@ -33,91 +49,124 @@ const proj = mat4.create();
|
|||||||
const view = mat4.create();
|
const view = mat4.create();
|
||||||
const mvp = mat4.create();
|
const mvp = mat4.create();
|
||||||
|
|
||||||
const vertex_src = `#version 300 es
|
// per frame state
|
||||||
precision mediump float;
|
let encoder: GPUCommandEncoder | undefined;
|
||||||
in vec3 vertexPosition;
|
let pass: GPURenderPassEncoder | undefined;
|
||||||
in vec2 vertexTexCoord;
|
let frame_view: GPUTextureView;
|
||||||
in vec4 vertexColor;
|
let msaa_texture: GPUTexture | undefined;
|
||||||
out vec2 fragTexCoord;
|
let depth_texture: GPUTexture | undefined;
|
||||||
out vec4 fragColor;
|
let current_pipeline: GPURenderPipeline | undefined;
|
||||||
uniform mat4 mvp;
|
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() {
|
const shader_src = /* wgsl */ `
|
||||||
fragTexCoord = vertexTexCoord;
|
struct Uniforms {
|
||||||
fragColor = vertexColor;
|
mvp: mat4x4<f32>,
|
||||||
gl_Position = mvp*vec4(vertexPosition, 1.0);
|
}
|
||||||
|
|
||||||
|
@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
|
export async function init_window(canvas_element: HTMLCanvasElement) {
|
||||||
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) {
|
|
||||||
canvas = canvas_element;
|
canvas = canvas_element;
|
||||||
canvas.width = 1800;
|
canvas.width = 1800;
|
||||||
canvas.height = 900;
|
canvas.height = 900;
|
||||||
|
|
||||||
const ctx = canvas.getContext("webgl2", { antialias: true, alpha: false });
|
if (!navigator.gpu) {
|
||||||
if (!ctx) {
|
throw new Error("WebGPU not supported");
|
||||||
throw new Error("WebGL2 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()!;
|
sampler = device.createSampler({
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, main_buffer);
|
magFilter: "nearest",
|
||||||
|
minFilter: "nearest",
|
||||||
|
addressModeU: "clamp-to-edge",
|
||||||
|
addressModeV: "clamp-to-edge",
|
||||||
|
});
|
||||||
|
|
||||||
const stride = FLOATS_PER_VERT * 4;
|
stream_buffer = create_stream_buffer(8 * 1024 * 1024);
|
||||||
|
create_uniform_buffer(64);
|
||||||
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);
|
|
||||||
|
|
||||||
create_white_texture();
|
create_white_texture();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function begin_drawing() {
|
export function begin_drawing() {
|
||||||
|
encoder = device.createCommandEncoder();
|
||||||
|
frame_view = context.getCurrentTexture().createView();
|
||||||
|
ensure_render_targets();
|
||||||
|
|
||||||
update_2d_mvp();
|
update_2d_mvp();
|
||||||
gl.depthFunc(gl.LEQUAL);
|
stream_offset = 0;
|
||||||
gl.clearDepth(1.0);
|
uniform_slot = -1;
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
write_mvp();
|
||||||
|
|
||||||
|
pending_color_clear = true;
|
||||||
|
pending_depth_clear = true;
|
||||||
vert_index = 0;
|
vert_index = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function end_drawing() {
|
export function end_drawing() {
|
||||||
flush_batch();
|
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() {
|
function update_2d_mvp() {
|
||||||
@@ -131,19 +180,23 @@ function update_2d_mvp() {
|
|||||||
const n = -1;
|
const n = -1;
|
||||||
const f = 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) {
|
export function begin_clip(x: number, y: number, width: number, height: number) {
|
||||||
gl.enable(gl.SCISSOR_TEST);
|
flush_batch();
|
||||||
|
scissor = { x, y, width, height };
|
||||||
const flipped_y = canvas.height - (y + height);
|
if (pass) {
|
||||||
|
apply_scissor(pass);
|
||||||
gl.scissor(x, flipped_y, width, height);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function end_clip() {
|
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) {
|
export function begin_mode_3d(new_camera: Camera) {
|
||||||
@@ -152,13 +205,8 @@ export function begin_mode_3d(new_camera: Camera) {
|
|||||||
mode3d = true;
|
mode3d = true;
|
||||||
camera = new_camera;
|
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();
|
update_camera();
|
||||||
|
write_mvp();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function end_mode_3d() {
|
export function end_mode_3d() {
|
||||||
@@ -169,15 +217,18 @@ export function end_mode_3d() {
|
|||||||
flush_batch();
|
flush_batch();
|
||||||
|
|
||||||
mode3d = false;
|
mode3d = false;
|
||||||
gl.disable(gl.DEPTH_TEST);
|
write_mvp();
|
||||||
gl.disable(gl.CULL_FACE);
|
|
||||||
gl.depthMask(false);
|
|
||||||
gl.disable(gl.POLYGON_OFFSET_FILL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clear_background(r: number, g: number, b: number, a = 1) {
|
export function clear_background(r: number, g: number, b: number, a = 1) {
|
||||||
gl.clearColor(r, g, b, a);
|
flush_batch();
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
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() {
|
export function flush_batch() {
|
||||||
@@ -185,52 +236,41 @@ export function flush_batch() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, main_buffer);
|
const byte_length = vert_index * 4;
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, vertex_data.subarray(0, vert_index), gl.STREAM_DRAW);
|
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
|
||||||
if (mode3d) {
|
pending_destroy.push(stream_buffer);
|
||||||
gl.uniformMatrix4fv(mvp_loc, false, mvp);
|
stream_buffer = create_stream_buffer(Math.max(stream_buffer.size * 2, byte_length));
|
||||||
} else {
|
stream_offset = 0;
|
||||||
gl.uniformMatrix4fv(mvp_loc, false, ortho);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stride = FLOATS_PER_VERT * 4;
|
device.queue.writeBuffer(stream_buffer, stream_offset, vertex_data, 0, vert_index);
|
||||||
gl.vertexAttribPointer(pos_loc, 3, gl.FLOAT, false, stride, 0);
|
draw(stream_buffer, stream_offset, vert_index / FLOATS_PER_VERT);
|
||||||
gl.vertexAttribPointer(uv_loc, 2, gl.FLOAT, false, stride, 12);
|
stream_offset += byte_length;
|
||||||
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);
|
|
||||||
|
|
||||||
vert_index = 0;
|
vert_index = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function flush_buffer(buffer: WebGLBuffer, draw_count: number) {
|
export function flush_buffer(buffer: GPUBuffer, draw_count: number) {
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
draw(buffer, 0, draw_count);
|
||||||
|
}
|
||||||
|
|
||||||
if (mode3d) {
|
export function create_vertex_buffer(vertices: Float32Array): GPUBuffer {
|
||||||
gl.uniformMatrix4fv(mvp_loc, false, mvp);
|
const buffer = device.createBuffer({
|
||||||
|
size: Math.max(4, vertices.byteLength),
|
||||||
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
||||||
|
});
|
||||||
|
device.queue.writeBuffer(buffer, 0, vertices);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
gl.uniformMatrix4fv(mvp_loc, false, ortho);
|
buffer.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
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, draw_count);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resize_canvas() {
|
export function resize_canvas() {
|
||||||
@@ -242,21 +282,26 @@ export function resize_canvas() {
|
|||||||
canvas.height = height;
|
canvas.height = height;
|
||||||
canvas.style.width = canvas.width + "px";
|
canvas.style.width = canvas.width + "px";
|
||||||
canvas.style.height = canvas.height + "px";
|
canvas.style.height = canvas.height + "px";
|
||||||
|
// render targets get recreated at the start of the next frame
|
||||||
gl.viewport(0, 0, width, height);
|
|
||||||
|
|
||||||
gl.useProgram(program);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function get_current_texture(): WebGLTexture | null {
|
export function get_current_texture(): GPUTexture | null {
|
||||||
return current_texture;
|
return current_texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function set_current_texture(texture: WebGLTexture) {
|
export function set_current_texture(texture: GPUTexture) {
|
||||||
current_texture = texture;
|
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) {
|
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;
|
let i = vert_index;
|
||||||
vertex_data[i++] = px;
|
vertex_data[i++] = px;
|
||||||
@@ -330,12 +375,115 @@ export function push_quad_vertices(
|
|||||||
|
|
||||||
// internal
|
// 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() {
|
function update_camera() {
|
||||||
if (!camera) {
|
if (!camera) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mat4.perspective(
|
mat4.perspectiveZO(
|
||||||
proj,
|
proj,
|
||||||
camera.fov,
|
camera.fov,
|
||||||
canvas.width / canvas.height,
|
canvas.width / canvas.height,
|
||||||
@@ -354,47 +502,107 @@ function update_camera() {
|
|||||||
mat4.multiply(mvp, proj, view);
|
mat4.multiply(mvp, proj, view);
|
||||||
}
|
}
|
||||||
|
|
||||||
function compile_shader(type: number, src: string) {
|
function create_pipelines() {
|
||||||
const shader = gl.createShader(type)!;
|
const module = device.createShaderModule({ code: shader_src });
|
||||||
|
|
||||||
gl.shaderSource(shader, src);
|
uniform_layout = device.createBindGroupLayout({
|
||||||
gl.compileShader(shader);
|
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)) {
|
const vertex: GPUVertexState = {
|
||||||
console.error(gl.getShaderInfoLog(shader));
|
module,
|
||||||
throw new Error("Shader compile failed");
|
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 };
|
||||||
|
|
||||||
return shader;
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function create_program(vs_src: string, fs_src: string) {
|
function create_stream_buffer(size: number) {
|
||||||
const vs = compile_shader(gl.VERTEX_SHADER, vs_src);
|
return device.createBuffer({ size, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
|
||||||
const fs = compile_shader(gl.FRAGMENT_SHADER, fs_src);
|
}
|
||||||
|
|
||||||
const program = gl.createProgram()!;
|
function create_uniform_buffer(slots: number) {
|
||||||
gl.attachShader(program, vs);
|
uniform_buffer = device.createBuffer({
|
||||||
gl.attachShader(program, fs);
|
size: slots * UNIFORM_SLOT_SIZE,
|
||||||
gl.linkProgram(program);
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
||||||
|
});
|
||||||
|
uniform_bind_group = device.createBindGroup({
|
||||||
|
layout: uniform_layout,
|
||||||
|
entries: [{ binding: 0, resource: { buffer: uniform_buffer, size: 64 } }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
function get_texture_bind_group(texture: GPUTexture) {
|
||||||
console.error(gl.getProgramInfoLog(program));
|
let bind_group = texture_bind_groups.get(texture);
|
||||||
throw new Error("Program link failed");
|
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;
|
||||||
return program;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function create_white_texture() {
|
function create_white_texture() {
|
||||||
const tex = gl.createTexture();
|
const tex = create_texture(1, 1);
|
||||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
device.queue.writeTexture({ texture: tex }, new Uint8Array([255, 255, 255, 255]), { bytesPerRow: 4 }, [1, 1]);
|
||||||
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);
|
|
||||||
|
|
||||||
white_tex = tex;
|
white_tex = tex;
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-22
@@ -1,28 +1,17 @@
|
|||||||
import { flush_batch, get_current_texture, gl, push_quad, push_quad_vertices, set_current_texture } from "./core.ts";
|
import {
|
||||||
|
create_texture,
|
||||||
|
device,
|
||||||
|
flush_batch,
|
||||||
|
get_current_texture,
|
||||||
|
push_quad,
|
||||||
|
push_quad_vertices,
|
||||||
|
set_current_texture,
|
||||||
|
} from "./core.ts";
|
||||||
import { Texture } from "./types.ts";
|
import { Texture } from "./types.ts";
|
||||||
|
|
||||||
export function load_texture(image: HTMLImageElement): Texture {
|
export function load_texture(image: HTMLImageElement): Texture {
|
||||||
const texture = gl.createTexture();
|
const texture = create_texture(image.width, image.height);
|
||||||
|
device.queue.copyExternalImageToTexture({ source: image }, { texture }, [image.width, image.height]);
|
||||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
||||||
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);
|
|
||||||
|
|
||||||
gl.texImage2D(
|
|
||||||
gl.TEXTURE_2D,
|
|
||||||
0,
|
|
||||||
gl.RGBA,
|
|
||||||
gl.RGBA,
|
|
||||||
gl.UNSIGNED_BYTE,
|
|
||||||
image,
|
|
||||||
);
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tex: texture,
|
tex: texture,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export interface Texture {
|
export interface Texture {
|
||||||
tex: WebGLTexture;
|
tex: GPUTexture;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { EverythingRegistry } from "$/common/everything_registry.ts";
|
|||||||
import { Chunk, Dimension } from "$/client/components/dimension.ts";
|
import { Chunk, Dimension } from "$/client/components/dimension.ts";
|
||||||
import { Camera } from "$/client/components/camera.ts";
|
import { Camera } from "$/client/components/camera.ts";
|
||||||
import { AssetManager } from "$/client/assets.ts";
|
import { AssetManager } from "$/client/assets.ts";
|
||||||
import { flush_buffer, gl, set_current_texture } from "$/client/renderer/mod.ts";
|
import { create_vertex_buffer, flush_buffer, set_current_texture } from "$/client/renderer/mod.ts";
|
||||||
|
|
||||||
let gdimension: Dimension;
|
let gdimension: Dimension;
|
||||||
|
|
||||||
@@ -20,25 +20,11 @@ worker.onmessage = (event) => {
|
|||||||
gdimension.delete_chunk_mesh(chunk);
|
gdimension.delete_chunk_mesh(chunk);
|
||||||
chunk.dirty = false;
|
chunk.dirty = false;
|
||||||
|
|
||||||
chunk.opaque_vertex_buffer = gl.createBuffer();
|
chunk.opaque_vertex_buffer = create_vertex_buffer(opaque_vertices.subarray(0, opaque_count));
|
||||||
chunk.opaque_vertex_count = opaque_count / 9;
|
chunk.opaque_vertex_count = opaque_count / 9;
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, chunk.opaque_vertex_buffer);
|
chunk.transparent_vertex_buffer = create_vertex_buffer(transparent_vertices.subarray(0, transparent_count));
|
||||||
gl.bufferData(
|
|
||||||
gl.ARRAY_BUFFER,
|
|
||||||
opaque_vertices.subarray(0, opaque_count),
|
|
||||||
gl.STATIC_DRAW,
|
|
||||||
);
|
|
||||||
|
|
||||||
chunk.transparent_vertex_buffer = gl.createBuffer();
|
|
||||||
chunk.transparent_vertex_count = transparent_count / 9;
|
chunk.transparent_vertex_count = transparent_count / 9;
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, chunk.transparent_vertex_buffer);
|
|
||||||
gl.bufferData(
|
|
||||||
gl.ARRAY_BUFFER,
|
|
||||||
transparent_vertices.subarray(0, transparent_count),
|
|
||||||
gl.STATIC_DRAW,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function strip_functions<T extends Record<string, any>>(obj: T) {
|
function strip_functions<T extends Record<string, any>>(obj: T) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"server": "deno run --allow-net --allow-read --allow-write --allow-env server/main.ts"
|
"server": "deno run --allow-net --allow-read --allow-write --allow-env server/main.ts"
|
||||||
},
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["dom", "dom.asynciterable", "dom.iterable", "deno.ns", "deno.unstable"]
|
"lib": ["dom", "dom.asynciterable", "dom.iterable", "deno.ns", "deno.unstable", "deno.webgpu"]
|
||||||
},
|
},
|
||||||
"unstable": ["bundle", "raw-imports"],
|
"unstable": ["bundle", "raw-imports"],
|
||||||
"fmt": {
|
"fmt": {
|
||||||
|
|||||||
Reference in New Issue
Block a user