import { mat4 } from 'https://webgpufundamentals.org/3rdparty/wgpu-matrix.module.js'; import { initializeWebGPU } from './wgpu-device.js'; import { createState } from './wgpu-state.js'; import { generateGlyphTextureAtlas, createTextureFromSource } from './wgpu-utility.js'; import { createPipeline } from './wgpu-pipeline.js'; import { fetchShaderCode } from './wgpu-shader.js'; import { GenerateVertexDataAndTexture } from './wgpu-texture.js'; import { generateGlyphVerticesForText } from './wgpu-text.js'; import { config } from './wgpu-config.js'; import { CANVAS, CTX, COLORS, RENDER_PASS_DESCRIPTOR } from './wgpu-constants.js'; // Canvas element for rendering const canvas = document.querySelector('canvas'); // State initialization const state = createState(config); const FIXED_DELTA_TIME = 1 / 60; // Fixed time step of 60 FPS for physics and game logic const MAX_FRAME_TIME = 0.25; // Maximum time to simulate per frame to prevent spiral of death const TARGET_FPS = 60; // Target frames per second const FRAME_DURATION = 1000 / TARGET_FPS; // Duration of a single frame in milliseconds async function Main() { const adapter = await navigator.gpu?.requestAdapter(); const { device, context, presentationFormat } = await initializeWebGPU(navigator, adapter, canvas); if (!device) return; state.device = device; // Initialize Resources await InitializeResources(presentationFormat); // Start the game loop GameLoop(context); } // Initialize shaders, pipeline, textures, and buffers async function InitializeResources(presentationFormat) { // Load shader code const shaderCode = await fetchShaderCode('shaders.wgsl'); const vertexSize = config.floatsPerVertex * 4; // Create rendering pipeline state.pipeline = await createPipeline(state.device, presentationFormat, vertexSize, shaderCode); // Generate glyph texture atlas const glyphCanvas = generateGlyphTextureAtlas(CANVAS, CTX, config); document.body.appendChild(glyphCanvas); glyphCanvas.style.backgroundColor = '#222'; // Create vertex and index buffers CreateBuffers(); // Generate vertex buffer data and texture GenerateVertexDataAndTexture(state, glyphCanvas, generateGlyphVerticesForText, COLORS, config, createTextureFromSource); } // Function to create vertex and index buffers function CreateBuffers() { const vertexBufferSize = config.maxGlyphs * config.vertsPerGlyph * config.floatsPerVertex * 4; state.vertexBuffer = state.device.createBuffer({ label: 'vertices', size: vertexBufferSize, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, }); state.indexBuffer = state.device.createBuffer({ label: 'indices', size: config.maxGlyphs * config.vertsPerGlyph * 4, usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST, }); const indices = GenerateIndices(config.maxGlyphs); state.device.queue.writeBuffer(state.indexBuffer, 0, new Uint32Array(indices)); } // Function to generate indices for glyphs function GenerateIndices(maxGlyphs) { // Generate index array for glyphs return Array.from({ length: maxGlyphs * 6 }, (_, i) => { const ndx = Math.floor(i / 6) * 4; return (i % 6 < 3 ? [ndx, ndx + 1, ndx + 2] : [ndx + 2, ndx + 1, ndx + 3])[i % 3]; }); } // Game loop function function GameLoop(context) { let lastTime = performance.now(); let accumulator = 0; function Tick() { const currentTime = performance.now(); const frameTime = (currentTime - lastTime) / 1000; lastTime = currentTime; const deltaTime = Math.min(frameTime, MAX_FRAME_TIME); accumulator += deltaTime; // Fixed time step updates for game logic while (accumulator >= FIXED_DELTA_TIME) { FixedUpdate(FIXED_DELTA_TIME); accumulator -= FIXED_DELTA_TIME; } // Variable time step update for rendering const alpha = accumulator / FIXED_DELTA_TIME; Render(alpha, context); // Schedule the next frame update setTimeout(Tick, FRAME_DURATION); } Tick(); } // Fixed update function for game logic function FixedUpdate(deltaTime) { state.time += deltaTime; // Perform game logic updates here, such as physics and AI } // Render function function Render(alpha, context) { // Set up projection and view matrices const fov = 60 * Math.PI / 180; const aspect = canvas.clientWidth / canvas.clientHeight; const projectionMatrix = mat4.perspective(fov, aspect, config.render.zNear, config.render.zFar); const viewMatrix = mat4.lookAt([0, 0, 5], [0, 0, 0], [0, 1, 0]); const viewProjectionMatrix = mat4.multiply(projectionMatrix, viewMatrix); RENDER_PASS_DESCRIPTOR.colorAttachments[0].view = context.getCurrentTexture().createView(); const encoder = state.device.createCommandEncoder(); const pass = encoder.beginRenderPass(RENDER_PASS_DESCRIPTOR); pass.setPipeline(state.pipeline); mat4.rotateY(viewProjectionMatrix, state.time, state.matrix); mat4.translate(state.matrix, [-state.width / 2, -state.height / 2, 0], state.matrix); state.device.queue.writeBuffer(state.uniformBuffer, 0, state.uniformValues); pass.setBindGroup(0, state.bindGroup); pass.setVertexBuffer(0, state.vertexBuffer); pass.setIndexBuffer(state.indexBuffer, 'uint32'); pass.drawIndexed(state.numGlyphs * 6); pass.end(); state.device.queue.submit([encoder.finish()]); } // Initialize application Main();