├── .gitignore ├── .prettierrc ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── assets ├── config │ └── renderer.config.json └── engine │ ├── fonts │ └── Exo-Medium.otf │ ├── models │ ├── cube │ │ ├── cube.bin │ │ └── cube.gltf │ └── sphere │ │ ├── sphere.bin │ │ └── sphere.gltf │ ├── shaders │ ├── common.wgsl │ ├── compute_raster_base.wgsl │ ├── cull.wgsl │ ├── deferred_lighting.wgsl │ ├── depth_only.wgsl │ ├── effects │ │ ├── bloom_downsample.wgsl │ │ ├── bloom_resolve.wgsl │ │ ├── bloom_upsample.wgsl │ │ ├── crt_post.wgsl │ │ ├── outline_post.wgsl │ │ ├── tilt_shift_post.wgsl │ │ ├── transform_ripples.wgsl │ │ └── vhs_post.wgsl │ ├── entity_prepass.wgsl │ ├── fullscreen.wgsl │ ├── gbuffer_base.wgsl │ ├── hzb_reduce.wgsl │ ├── lighting_common.wgsl │ ├── line.wgsl │ ├── postprocess_common.wgsl │ ├── skybox.wgsl │ ├── standard_material.wgsl │ ├── system_compute │ │ ├── aabb_raycast.wgsl │ │ ├── aabb_tree_processing.wgsl │ │ ├── bounds_processing.wgsl │ │ ├── clear_dirty_flags.wgsl │ │ ├── compact_lights.wgsl │ │ ├── decompose_transform.wgsl │ │ ├── line_transform_processing.wgsl │ │ ├── particle_render.wgsl │ │ └── transform_processing.wgsl │ ├── text_material.wgsl │ ├── transparency_composite.wgsl │ └── ui_standard_material.wgsl │ ├── sprites │ └── cursor.png │ └── textures │ ├── gradientbox │ ├── nx.png │ ├── ny.png │ ├── nz.png │ ├── px.png │ ├── py.png │ └── pz.png │ ├── simple_skybox │ ├── nx.png │ ├── ny.png │ ├── nz.png │ ├── px.png │ ├── py.png │ └── pz.png │ ├── spacebox │ ├── nx.png │ ├── ny.png │ ├── nz.png │ ├── px.png │ ├── py.png │ └── pz.png │ ├── voxel │ ├── dirt_albedo.jpg │ └── dirt_roughness.jpg │ ├── wall │ ├── wall_albedo.png │ ├── wall_ao.png │ ├── wall_height.png │ ├── wall_metallic.png │ ├── wall_normal.png │ └── wall_roughness.png │ └── worn_panel │ ├── worn_panel_albedo.png │ ├── worn_panel_ao.png │ ├── worn_panel_height.png │ ├── worn_panel_metallic.png │ ├── worn_panel_normal.png │ └── worn_panel_roughness.png ├── electron.vite.config.mjs ├── electron ├── main │ └── main.js ├── preload │ └── preload.js └── third_party │ └── webgpu_inspector │ ├── background.js │ ├── content_script.js │ ├── manifest.json │ ├── res │ ├── webgpu_inspector_on-19.png │ └── webgpu_inspector_on-38.png │ ├── webgpu_inspector.js │ ├── webgpu_inspector_devtools.html │ ├── webgpu_inspector_devtools.js │ ├── webgpu_inspector_loader.js │ ├── webgpu_inspector_panel.css │ ├── webgpu_inspector_panel.html │ ├── webgpu_inspector_window.js │ ├── webgpu_inspector_worker.js │ ├── webgpu_recorder.js │ └── webgpu_recorder_loader.js ├── engine └── src │ ├── acceleration │ ├── aabb.js │ ├── aabb_gpu_raycast.js │ ├── aabb_raycast.js │ └── aabb_tree_processor.js │ ├── core │ ├── application_state.js │ ├── config_db.js │ ├── dispatcher.js │ ├── ecs │ │ ├── entity.js │ │ ├── entity_utils.js │ │ ├── fragment.js │ │ ├── fragment_registry.js │ │ ├── fragments │ │ │ ├── fragment_definitions.js │ │ │ ├── light_fragment.js │ │ │ ├── static_mesh_fragment.js │ │ │ ├── text_fragment.js │ │ │ ├── transform_fragment.js │ │ │ ├── user_interface_fragment.js │ │ │ └── visibility_fragment.js │ │ ├── meta │ │ │ ├── fragment_generator.js │ │ │ └── fragment_generator_types.js │ │ └── solar │ │ │ ├── archetype.js │ │ │ ├── chunk.js │ │ │ ├── memory.js │ │ │ ├── query.js │ │ │ ├── sector.js │ │ │ ├── types.js │ │ │ └── view.js │ ├── minimal.js │ ├── scene.js │ ├── scene_graph.js │ ├── shared_data.js │ ├── simulation_core.js │ ├── simulation_layer.js │ ├── simulator.js │ └── subsystems │ │ ├── aabb_debug_renderer.js │ │ ├── aabb_entity_adapter.js │ │ ├── entity_preprocessor.js │ │ ├── freeform_arcball_control_processor.js │ │ ├── static_mesh_processor.js │ │ ├── text_processor.js │ │ ├── transform_processor.js │ │ ├── ui_3d_processor.js │ │ ├── ui_processor.js │ │ └── view_processor.js │ ├── input │ ├── input_context.js │ ├── input_processor.js │ ├── input_provider.js │ └── input_types.js │ ├── memory │ ├── allocator.js │ └── container.js │ ├── meta │ └── meta_system.js │ ├── ml │ ├── layer.js │ ├── layers │ │ ├── binary_cross_entropy_loss.js │ │ ├── cross_entropy_loss.js │ │ ├── fully_connected.js │ │ ├── input.js │ │ ├── mse_loss.js │ │ ├── relu.js │ │ ├── sigmoid.js │ │ ├── softmax.js │ │ └── tanh.js │ ├── logger.js │ ├── mastermind.js │ ├── math │ │ └── tensor.js │ ├── ml_types.js │ ├── names.js │ ├── neural_architecture.js │ ├── ops │ │ ├── hop_api_adapter.js │ │ ├── op_store.js │ │ ├── op_types.js │ │ └── ops.js │ ├── optimizer.js │ └── optimizers │ │ └── adam.js │ ├── renderer │ ├── bind_group.js │ ├── buffer.js │ ├── command_queue.js │ ├── compute_raster_task_queue.js │ ├── compute_task_queue.js │ ├── line_renderer.js │ ├── material.js │ ├── mesh.js │ ├── mesh_task_queue.js │ ├── pipeline_state.js │ ├── post_process_stack.js │ ├── render_graph.js │ ├── render_pass.js │ ├── renderer.js │ ├── renderer_types.js │ ├── resource_cache.js │ ├── shader.js │ ├── strategies │ │ └── deferred_shading.js │ └── texture.js │ ├── tools │ ├── aabb_debug.js │ ├── camera_info.js │ ├── dev_console.js │ ├── dev_console_tool.js │ ├── ml_stats.js │ ├── performance_trace.js │ └── render_pass_organizer.js │ ├── ui │ ├── 2d │ │ └── immediate.js │ ├── 3d │ │ ├── element.js │ │ └── element_pool.js │ └── text │ │ ├── font.js │ │ └── font_cache.js │ └── utility │ ├── camera.js │ ├── color.js │ ├── config_permutations.js │ ├── execution_queue.js │ ├── file_system.js │ ├── frame_runner.js │ ├── gltf_loader.js │ ├── hashing.js │ ├── linear.js │ ├── logging.js │ ├── math.js │ ├── names.js │ ├── object.js │ ├── performance.js │ └── state_machine.js ├── example └── app.js ├── favicon.ico ├── forge.config.js ├── index.html ├── package-lock.json ├── package.json ├── styles.css ├── sundown.code-workspace ├── sundown_demo.gif ├── tools ├── dev_server.js ├── fragment_preprocessor.js └── msdf_font_generator.js └── vite.config.mjs /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .gitignore 3 | executables/ 4 | dist/ 5 | out/ 6 | 7 | **/fonts/**/*.json 8 | **/fonts/**/*.png 9 | **/config/**/*.json 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "tabWidth": 2, 5 | "printWidth": 100, 6 | "trailingComma": "es5" 7 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Sundown Localhost", 11 | "url": "http://localhost:5173", 12 | "webRoot": "${workspaceFolder}", 13 | "resolveSourceMapLocations": [ 14 | "${workspaceFolder}/**", 15 | "!**/node_modules/**" 16 | ], 17 | "sourceMaps": true, 18 | "sourceMapPathOverrides": { 19 | "webpack:///./*": "${webRoot}/*", 20 | "webpack:///src/*": "${webRoot}/src/*", 21 | "webpack:////*": "*" 22 | }, 23 | "preLaunchTask": "npm: dev", 24 | "userDataDir": false, 25 | "skipFiles": [ 26 | "/**" 27 | ] 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Adrian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sundown Engine 🕹️ 2 | 3 | An extendible WebGPU game and simulation engine for fun, games and research. 4 | 5 | ![Sundown Demo](./sundown_demo.gif) 6 | 7 | Some of the current (code) features include: 8 | 9 | * ⚡ WebGPU renderable abstractions 10 | * ⚡ Flexible render graph for crafting render and compute pipelines 11 | * ⚡ Simple, expressive material system for crafting custom shaders and materials 12 | * ⚡ Gameplay simulation layer system for adding layered, modular functionality 13 | * ⚡ ECS system for more efficient processing, using a fragment framework and TypedArrays where possible 14 | * ⚡ Simple, context-based input system, allowing you to set up different input schemes and contexts 15 | * ⚡ Built-in PBR shaders 16 | * ⚡ Entity-first instancing 17 | * ⚡ Auto instancing and draw batching of meshes using a specialized mesh task queue 18 | * ⚡ Compute task queue for easily submitting compute shader work 19 | * ⚡ MSDF text rendering 20 | * ⚡ Configurable post-process stack 21 | * ⚡ Immediate mode screen-space UI 22 | * ⚡ Dynamic AABB tree acceleration structure and ray casting 23 | * ⚡ Helpers for loading GTLFs, tracking performance scopes, named IDs, running frames and more. 24 | 25 | Sundown also includes a simple but capable ML framework for running real-time AI experiments: 26 | * ⚡ Simple gradient tape for backprop based learning 27 | * ⚡ High-level, layer-based DAG subnet API for composing models from smaller subnetworks 28 | * ⚡ Expanding library of activation functions, loss functions, optimizers and configurable layers 29 | * ⚡ MasterMind class for orchestrating weight sharing, adaptation and real-time retraining of multiple models 30 | 31 | ### Installation 32 | 33 | 34 | Make sure you have the latest version of [NodeJS](https://nodejs.org/en) installed. Clone this repository and make sure to `npm install` to get all the package dependencies. 35 | 36 | 37 | ```bash 38 | > git clone git@github.com:Sunset-Studio/Sundown.git 39 | > cd Sundown 40 | > npm install 41 | ``` 42 | 43 | ### Running 44 | 45 | 46 | With the project cloned and all package dependencies installed, you're ready to run the project. There is an example **app.js** that is included from the top-level **index.html** file. Feel free to replace this with your own experiments or entry points. 47 | 48 | 49 | To run the development project in a browser, use the npm `dev` command 50 | ```bash 51 | > npm run dev 52 | ``` 53 | 54 | 55 | To run the development project in an electron instance, use the npm `devtop` command 56 | ```bash 57 | > npm run devtop 58 | ``` 59 | 60 | 61 | ### Packaging 62 | 63 | 64 | You can package and distribute builds for the web or for desktop with the help of [Electron Forge](https://www.electronforge.io/). 65 | 66 | 67 | To package for the web, just run the npm `build` command. 68 | ```bash 69 | > npm run build 70 | ``` 71 | 72 | 73 | Then copy the resulting **index.html** file and **assets** and **engine** directories into your site's root. 74 | 75 | 76 | To build executable electron packages, use the provided npm `make` command. This will create executable outputs in a top level *executables* directory. 77 | ```bash 78 | > npm run make 79 | ``` 80 | 81 | 82 | ### Contributing 83 | 84 | 85 | Sundown is available for free under the MIT license. You can use and modify the engine for individual or commercial use (a reference or mention is still appreciated!) If you want to contribute features or fixes, please fork this repository and submit PRs. I am a one man team but will check any promising PRs as soon as I can. If you want to become a regular contributor feel free to DM me on [X](https://x.com/SunsetLearn) or shoot me an email at adrians.sanchez@sunsetlearn.com. 86 | -------------------------------------------------------------------------------- /assets/engine/fonts/Exo-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/fonts/Exo-Medium.otf -------------------------------------------------------------------------------- /assets/engine/models/cube/cube.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/models/cube/cube.bin -------------------------------------------------------------------------------- /assets/engine/models/cube/cube.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset":{ 3 | "generator":"Khronos glTF Blender I/O v4.1.63", 4 | "version":"2.0" 5 | }, 6 | "scene":0, 7 | "scenes":[ 8 | { 9 | "name":"Scene", 10 | "nodes":[ 11 | 0 12 | ] 13 | } 14 | ], 15 | "nodes":[ 16 | { 17 | "mesh":0, 18 | "name":"Cube" 19 | } 20 | ], 21 | "materials":[ 22 | { 23 | "doubleSided":true, 24 | "name":"Material", 25 | "pbrMetallicRoughness":{ 26 | "baseColorFactor":[ 27 | 0.800000011920929, 28 | 0.800000011920929, 29 | 0.800000011920929, 30 | 1 31 | ], 32 | "metallicFactor":0, 33 | "roughnessFactor":0.5 34 | } 35 | } 36 | ], 37 | "meshes":[ 38 | { 39 | "name":"Cube", 40 | "primitives":[ 41 | { 42 | "attributes":{ 43 | "POSITION":0, 44 | "NORMAL":1, 45 | "TEXCOORD_0":2 46 | }, 47 | "indices":3, 48 | "material":0 49 | } 50 | ] 51 | } 52 | ], 53 | "accessors":[ 54 | { 55 | "bufferView":0, 56 | "componentType":5126, 57 | "count":24, 58 | "max":[ 59 | 1, 60 | 1, 61 | 1 62 | ], 63 | "min":[ 64 | -1, 65 | -1, 66 | -1 67 | ], 68 | "type":"VEC3" 69 | }, 70 | { 71 | "bufferView":1, 72 | "componentType":5126, 73 | "count":24, 74 | "type":"VEC3" 75 | }, 76 | { 77 | "bufferView":2, 78 | "componentType":5126, 79 | "count":24, 80 | "type":"VEC2" 81 | }, 82 | { 83 | "bufferView":3, 84 | "componentType":5123, 85 | "count":36, 86 | "type":"SCALAR" 87 | } 88 | ], 89 | "bufferViews":[ 90 | { 91 | "buffer":0, 92 | "byteLength":288, 93 | "byteOffset":0, 94 | "target":34962 95 | }, 96 | { 97 | "buffer":0, 98 | "byteLength":288, 99 | "byteOffset":288, 100 | "target":34962 101 | }, 102 | { 103 | "buffer":0, 104 | "byteLength":192, 105 | "byteOffset":576, 106 | "target":34962 107 | }, 108 | { 109 | "buffer":0, 110 | "byteLength":72, 111 | "byteOffset":768, 112 | "target":34963 113 | } 114 | ], 115 | "buffers":[ 116 | { 117 | "byteLength":840, 118 | "uri":"cube.bin" 119 | } 120 | ] 121 | } 122 | -------------------------------------------------------------------------------- /assets/engine/models/sphere/sphere.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/models/sphere/sphere.bin -------------------------------------------------------------------------------- /assets/engine/models/sphere/sphere.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset":{ 3 | "generator":"Khronos glTF Blender I/O v4.1.63", 4 | "version":"2.0" 5 | }, 6 | "scene":0, 7 | "scenes":[ 8 | { 9 | "name":"Scene", 10 | "nodes":[ 11 | 0 12 | ] 13 | } 14 | ], 15 | "nodes":[ 16 | { 17 | "mesh":0, 18 | "name":"Sphere" 19 | } 20 | ], 21 | "meshes":[ 22 | { 23 | "name":"Sphere", 24 | "primitives":[ 25 | { 26 | "attributes":{ 27 | "POSITION":0, 28 | "NORMAL":1, 29 | "TEXCOORD_0":2, 30 | "TANGENT":3 31 | }, 32 | "indices":4 33 | } 34 | ] 35 | } 36 | ], 37 | "accessors":[ 38 | { 39 | "bufferView":0, 40 | "componentType":5126, 41 | "count":167, 42 | "max":[ 43 | 1, 44 | 1, 45 | 1 46 | ], 47 | "min":[ 48 | -1, 49 | -1, 50 | -1 51 | ], 52 | "type":"VEC3" 53 | }, 54 | { 55 | "bufferView":1, 56 | "componentType":5126, 57 | "count":167, 58 | "type":"VEC3" 59 | }, 60 | { 61 | "bufferView":2, 62 | "componentType":5126, 63 | "count":167, 64 | "type":"VEC2" 65 | }, 66 | { 67 | "bufferView":3, 68 | "componentType":5126, 69 | "count":167, 70 | "type":"VEC4" 71 | }, 72 | { 73 | "bufferView":4, 74 | "componentType":5123, 75 | "count":792, 76 | "type":"SCALAR" 77 | } 78 | ], 79 | "bufferViews":[ 80 | { 81 | "buffer":0, 82 | "byteLength":2004, 83 | "byteOffset":0, 84 | "target":34962 85 | }, 86 | { 87 | "buffer":0, 88 | "byteLength":2004, 89 | "byteOffset":2004, 90 | "target":34962 91 | }, 92 | { 93 | "buffer":0, 94 | "byteLength":1336, 95 | "byteOffset":4008, 96 | "target":34962 97 | }, 98 | { 99 | "buffer":0, 100 | "byteLength":2672, 101 | "byteOffset":5344, 102 | "target":34962 103 | }, 104 | { 105 | "buffer":0, 106 | "byteLength":1584, 107 | "byteOffset":8016, 108 | "target":34963 109 | } 110 | ], 111 | "buffers":[ 112 | { 113 | "byteLength":9600, 114 | "uri":"sphere.bin" 115 | } 116 | ] 117 | } 118 | -------------------------------------------------------------------------------- /assets/engine/shaders/compute_raster_base.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | 3 | // ------------------------------------------------------------------------------------ 4 | // Data Structures 5 | // ------------------------------------------------------------------------------------ 6 | 7 | struct Vertex { 8 | position: vec4f, 9 | uv: vec2f, 10 | normal: vec4f, 11 | }; 12 | 13 | struct Fragment { 14 | color: vec4f, 15 | depth: f32, 16 | }; 17 | 18 | // ------------------------------------------------------------------------------------ 19 | // Constants 20 | // ------------------------------------------------------------------------------------ 21 | 22 | const workgroup_size = 256; 23 | 24 | // ------------------------------------------------------------------------------------ 25 | // Buffers 26 | // ------------------------------------------------------------------------------------ 27 | 28 | @group(0) @binding(0) var vertices: array; 29 | @group(0) @binding(1) var indices: array; 30 | 31 | @group(0) @binding(2) var framebuffer: array; 32 | @group(0) @binding(3) var depthbuffer: array; 33 | 34 | // ------------------------------------------------------------------------------------ 35 | // Rasterization Methods 36 | // ------------------------------------------------------------------------------------ 37 | 38 | fn rasterize_point(position: vec2f, color: vec4f) { 39 | let index = u32(position.y) * workgroup_size + u32(position.x); 40 | if (index < framebuffer.length()) { 41 | framebuffer[index] = color; 42 | depthbuffer[index] = 0.0; // Example depth 43 | } 44 | } 45 | 46 | fn rasterize_line(start: vec2f, end: vec2f, color: vec4f) { 47 | // Implement Bresenham's line algorithm or similar 48 | // ... 49 | } 50 | 51 | fn rasterize_triangle(v0: vec2f, v1: vec2f, v2: vec2f, color: vec4f) { 52 | // Implement triangle rasterization (e.g., barycentric coordinates) 53 | // ... 54 | } 55 | 56 | fn rasterize_quad(v0: vec2f, v1: vec2f, v2: vec2f, v3: vec2f, color: vec4f) { 57 | // Rasterize two triangles to form a quad 58 | rasterize_triangle(v0, v1, v2, color); 59 | rasterize_triangle(v2, v3, v0, color); 60 | } 61 | 62 | // ------------------------------------------------------------------------------------ 63 | // Compute Shader Entry 64 | // ------------------------------------------------------------------------------------ 65 | 66 | #ifndef CUSTOM_RASTER 67 | fn rasterize(v_out: VertexOutput, f_out: ptr) -> FragmentOutput { 68 | return *f_out; 69 | } 70 | #endif 71 | 72 | @compute @workgroup_size(workgroup_size) 73 | fn cs(@builtin(global_invocation_id) global_id: vec3) { 74 | let x = global_id.x; 75 | let y = global_id.y; 76 | 77 | rasterize(vec2f(x, y), vec4f(1.0, 0.0, 0.0, 1.0)); 78 | } 79 | 80 | 81 | -------------------------------------------------------------------------------- /assets/engine/shaders/deferred_lighting.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | #include "lighting_common.wgsl" 3 | 4 | // ------------------------------------------------------------------------------------ 5 | // Buffers 6 | // ------------------------------------------------------------------------------------ 7 | 8 | @group(1) @binding(0) var skybox_texture: texture_2d; 9 | @group(1) @binding(1) var albedo_texture: texture_2d; 10 | @group(1) @binding(2) var emissive_texture: texture_2d; 11 | @group(1) @binding(3) var smra_texture: texture_2d; 12 | @group(1) @binding(4) var normal_texture: texture_2d; 13 | @group(1) @binding(5) var position_texture: texture_2d; 14 | @group(1) @binding(6) var depth_texture: texture_depth_2d; 15 | @group(1) @binding(7) var dense_lights_buffer: array; 16 | @group(1) @binding(8) var light_count_buffer: array; 17 | 18 | // ------------------------------------------------------------------------------------ 19 | // Data Structures 20 | // ------------------------------------------------------------------------------------ 21 | 22 | struct VertexOutput { 23 | @builtin(position) position: vec4f, 24 | @location(0) uv: vec2, 25 | }; 26 | 27 | struct FragmentOutput { 28 | @location(0) color: vec4, 29 | }; 30 | 31 | 32 | // ------------------------------------------------------------------------------------ 33 | // Vertex Shader 34 | // ------------------------------------------------------------------------------------ 35 | 36 | @vertex fn vs( 37 | @builtin(vertex_index) vi : u32, 38 | @builtin(instance_index) ii: u32 39 | ) -> VertexOutput { 40 | var output : VertexOutput; 41 | output.position = vec4(vertex_buffer[vi].position); 42 | output.uv = vertex_buffer[vi].uv; 43 | return output; 44 | } 45 | 46 | // ------------------------------------------------------------------------------------ 47 | // Fragment Shader 48 | // ------------------------------------------------------------------------------------ 49 | 50 | @fragment fn fs(v_out: VertexOutput) -> FragmentOutput { 51 | let ambient = vec3(0.2, 0.2, 0.2); 52 | let uv = vec2(v_out.uv); 53 | 54 | var tex_sky = textureSample(skybox_texture, global_sampler, uv); 55 | 56 | var tex_albedo = textureSample(albedo_texture, global_sampler, uv); 57 | var albedo = tex_albedo.rgb; 58 | 59 | var tex_emissive = textureSample(emissive_texture, global_sampler, uv); 60 | var emissive = tex_emissive.r; 61 | 62 | var tex_normal = textureSample(normal_texture, global_sampler, uv); 63 | var normal = tex_normal.xyz; 64 | var normal_length = length(normal); 65 | var normalized_normal = normal / normal_length; 66 | var deferred_standard_lighting = tex_normal.w; 67 | 68 | var tex_smra = textureSample(smra_texture, global_sampler, uv); 69 | var reflectance = tex_smra.r * 0.0009765625 /* 1.0f / 1024 */; 70 | var metallic = tex_smra.g; 71 | var roughness = tex_smra.b; 72 | var ao = tex_smra.a; 73 | 74 | var tex_position = textureSample(position_texture, global_sampler, uv); 75 | var position = tex_position.xyz; 76 | 77 | var view_dir = normalize(view_buffer[0].view_position.xyz - position); 78 | 79 | let unlit = min(1u, u32(normal_length <= 0.0) + u32(1.0 - deferred_standard_lighting)); 80 | 81 | var color = f32(unlit) * tex_sky.rgb * mix(vec3f(1.0), albedo, tex_albedo.a); 82 | 83 | let num_lights = light_count_buffer[0] * (1u - unlit); 84 | for (var i = 0u; i < num_lights; i++) { 85 | var light = dense_lights_buffer[i]; 86 | if (light.activated <= 0.0) { 87 | continue; 88 | } 89 | color += calculate_brdf( 90 | light, 91 | normalized_normal, 92 | view_dir, 93 | position, 94 | albedo, 95 | roughness, 96 | metallic, 97 | reflectance, 98 | 0.0, // clear coat 99 | 1.0, // clear coat roughness 100 | ao, 101 | vec3f(1.0, 1.0, 1.0), // irradiance 102 | vec3f(1.0, 1.0, 1.0), // prefilter color 103 | vec2f(1.0, 1.0), // env brdf 104 | 0, // shadow map index 105 | ); 106 | } 107 | 108 | color += (emissive * albedo); 109 | 110 | return FragmentOutput(vec4(vec4(color, 1.0))); 111 | } -------------------------------------------------------------------------------- /assets/engine/shaders/depth_only.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | 3 | // ------------------------------------------------------------------------------------ 4 | // Data Structures 5 | // ------------------------------------------------------------------------------------ 6 | struct VertexOutput { 7 | @builtin(position) @invariant position: vec4f, 8 | }; 9 | 10 | // ------------------------------------------------------------------------------------ 11 | // Buffers 12 | // ------------------------------------------------------------------------------------ 13 | 14 | @group(1) @binding(0) var entity_transforms: array; 15 | @group(1) @binding(1) var entity_flags: array; 16 | @group(1) @binding(2) var compacted_object_instances: array; 17 | 18 | // ------------------------------------------------------------------------------------ 19 | // Vertex Shader 20 | // ------------------------------------------------------------------------------------ 21 | @vertex fn vs( 22 | @builtin(vertex_index) vi : u32, 23 | @builtin(instance_index) ii: u32 24 | ) -> VertexOutput { 25 | let instance_vertex = vertex_buffer[vi]; 26 | let entity_resolved = get_entity_row(compacted_object_instances[ii].row); 27 | let transform = entity_transforms[entity_resolved].transform; 28 | let view_proj_mat = view_buffer[0].view_projection_matrix; 29 | 30 | let is_bill = (entity_flags[entity_resolved] & EF_BILLBOARD) != 0; 31 | 32 | let world_pos = select( 33 | view_proj_mat * transform * instance_vertex.position, 34 | view_proj_mat * billboard_vertex_local( 35 | instance_vertex.uv, 36 | transform 37 | ), 38 | is_bill 39 | ); 40 | 41 | var output : VertexOutput; 42 | output.position = world_pos; 43 | return output; 44 | } -------------------------------------------------------------------------------- /assets/engine/shaders/effects/bloom_downsample.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | 3 | struct BloomBlurConstants { 4 | input_texture_size: vec2, 5 | output_texture_size: vec2, 6 | blur_radius: f32, 7 | mip_index: u32, 8 | } 9 | 10 | @group(1) @binding(0) var input_texture: texture_2d; 11 | @group(1) @binding(1) var output_texture: texture_storage_2d; 12 | @group(1) @binding(2) var bloom_blur_constants: BloomBlurConstants; 13 | 14 | // Adapted from https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom 15 | // This shader performs downsampling on a texture, 16 | // as taken from Call Of Duty method, presented at ACM Siggraph 2014. 17 | // This particular method was customly designed to eliminate 18 | // "pulsating artifacts and temporal stability issues". 19 | 20 | @compute @workgroup_size(16, 16, 1) 21 | fn cs(@builtin(global_invocation_id) global_id: vec3) { 22 | let source_texel_size = 1.0 / bloom_blur_constants.input_texture_size; 23 | let target_texel_size = 1.0 / bloom_blur_constants.output_texture_size; 24 | 25 | let texture_coord = (vec2f(global_id.xy) + vec2f(0.5)) * target_texel_size; 26 | 27 | if (global_id.x < u32(bloom_blur_constants.output_texture_size.x) && 28 | global_id.y < u32(bloom_blur_constants.output_texture_size.y)) { 29 | // Take 13 samples around current texel: 30 | // a - b - c 31 | // - j - k - 32 | // d - e - f 33 | // - l - m - 34 | // g - h - i 35 | // === ('e' is the current texel) === 36 | let a = textureSampleLevel(input_texture, clamped_sampler, vec2f(texture_coord.x - 2.0 * source_texel_size.x, texture_coord.y + 2.0 * source_texel_size.y), 0).rgb; 37 | let b = textureSampleLevel(input_texture, clamped_sampler, vec2f(texture_coord.x, texture_coord.y + 2.0 * source_texel_size.y), 0).rgb; 38 | let c = textureSampleLevel(input_texture, clamped_sampler, vec2f(texture_coord.x + 2.0 * source_texel_size.x, texture_coord.y + 2.0 * source_texel_size.y), 0).rgb; 39 | 40 | let d = textureSampleLevel(input_texture, clamped_sampler, vec2f(texture_coord.x - 2.0 * source_texel_size.x, texture_coord.y), 0).rgb; 41 | let e = textureSampleLevel(input_texture, clamped_sampler, vec2f(texture_coord.x, texture_coord.y), 0).rgb; 42 | let f = textureSampleLevel(input_texture, clamped_sampler, vec2f(texture_coord.x + 2.0 * source_texel_size.x, texture_coord.y), 0).rgb; 43 | 44 | let g = textureSampleLevel(input_texture, clamped_sampler, vec2f(texture_coord.x - 2.0 * source_texel_size.x, texture_coord.y - 2.0 * source_texel_size.y), 0).rgb; 45 | let h = textureSampleLevel(input_texture, clamped_sampler, vec2f(texture_coord.x, texture_coord.y - 2.0 * source_texel_size.y), 0).rgb; 46 | let i = textureSampleLevel(input_texture, clamped_sampler, vec2f(texture_coord.x + 2.0 * source_texel_size.x, texture_coord.y - 2.0 * source_texel_size.y), 0).rgb; 47 | 48 | let j = textureSampleLevel(input_texture, clamped_sampler, vec2f(texture_coord.x - 1.0 * source_texel_size.x, texture_coord.y + 1.0 * source_texel_size.y), 0).rgb; 49 | let k = textureSampleLevel(input_texture, clamped_sampler, vec2f(texture_coord.x + 1.0 * source_texel_size.x, texture_coord.y + 1.0 * source_texel_size.y), 0).rgb; 50 | let l = textureSampleLevel(input_texture, clamped_sampler, vec2f(texture_coord.x - 1.0 * source_texel_size.x, texture_coord.y - 1.0 * source_texel_size.y), 0).rgb; 51 | let m = textureSampleLevel(input_texture, clamped_sampler, vec2f(texture_coord.x + 1.0 * source_texel_size.x, texture_coord.y - 1.0 * source_texel_size.y), 0).rgb; 52 | 53 | // Apply weighted distribution: 54 | // 0.5 + 0.125 + 0.125 + 0.125 + 0.125 = 1 55 | // a,b,d,e * 0.125 56 | // b,c,e,f * 0.125 57 | // d,e,g,h * 0.125 58 | // e,f,h,i * 0.125 59 | // j,k,l,m * 0.5 60 | // This shows 5 square areas that are being sampled. But some of them overlap, 61 | // so to have an energy preserving downsample we need to make some adjustments. 62 | // The weights are the distributed, so that the sum of j,k,l,m (e.g.) 63 | // contribute 0.5 to the final color output. The code below is written 64 | // to effectively yield this sum. We get: 65 | // 0.125*5 + 0.03125*4 + 0.0625*4 = 1 66 | 67 | var downsample_color = e * 0.125; 68 | downsample_color += (a + c + g + i) * 0.03125; 69 | downsample_color += (b + d + f + h) * 0.0625; 70 | downsample_color += (j + k + l + m) * 0.125; 71 | downsample_color = max(downsample_color, vec3f(0.0001)); 72 | 73 | textureStore(output_texture, vec2i(global_id.xy), vec4f(downsample_color, 1.0)); 74 | } 75 | } -------------------------------------------------------------------------------- /assets/engine/shaders/effects/bloom_resolve.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | #include "postprocess_common.wgsl" 3 | 4 | struct VertexOutput { 5 | @builtin(position) position: vec4, 6 | @location(0) uv: vec2, 7 | @location(1) @interpolate(flat) instance_index: u32, 8 | } 9 | 10 | struct BloomResolveConstants { 11 | exposure: f32, 12 | bloom_intensity: f32, 13 | bloom_threshold: f32, 14 | bloom_knee: f32, 15 | } 16 | 17 | @group(1) @binding(0) var scene_color: texture_2d; 18 | @group(1) @binding(1) var bloom_brightness: texture_2d; 19 | @group(1) @binding(2) var bloom_resolve_constants: BloomResolveConstants; 20 | 21 | fn luminance(color: vec3) -> f32 { 22 | return dot(color, vec3(0.2126, 0.7152, 0.0722)); 23 | } 24 | 25 | fn apply_bloom(scene: vec3, bloom: vec3, intensity: f32, threshold: f32, knee: f32) -> vec3 { 26 | let scene_luminance = luminance(scene); 27 | let bloom_luminance = luminance(bloom); 28 | 29 | // Soft threshold 30 | let soft_threshold = smoothstep(threshold - knee, threshold + knee, scene_luminance); 31 | 32 | // Non-linear intensity scaling based on scene brightness 33 | let adjusted_intensity = intensity * pow(soft_threshold, 2.0); 34 | 35 | return mix(scene, scene + bloom * adjusted_intensity, soft_threshold); 36 | } 37 | 38 | @fragment 39 | fn fs(in: VertexOutput) -> @location(0) vec4 { 40 | let uv = vec2(in.uv); 41 | var color = textureSample(scene_color, global_sampler, uv).rgb; 42 | let bloom_color = textureSample(bloom_brightness, global_sampler, uv).rgb; 43 | 44 | color = apply_bloom( 45 | color, 46 | bloom_color, 47 | bloom_resolve_constants.bloom_intensity, 48 | bloom_resolve_constants.bloom_threshold, 49 | bloom_resolve_constants.bloom_knee 50 | ); 51 | 52 | color = reinhard_tonemapping(color, bloom_resolve_constants.exposure); 53 | 54 | return vec4(vec4(color, 1.0)); 55 | } -------------------------------------------------------------------------------- /assets/engine/shaders/effects/bloom_upsample.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | 3 | struct BloomBlurConstants { 4 | input_texture_size: vec2, 5 | output_texture_size: vec2, 6 | bloom_filter_radius: f32, 7 | mip_index: u32, 8 | } 9 | 10 | @group(1) @binding(0) var input_texture: texture_2d; 11 | @group(1) @binding(1) var output_texture: texture_storage_2d; 12 | @group(1) @binding(2) var bloom_blur_constants: BloomBlurConstants; 13 | 14 | // Adapted from https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom 15 | // This shader performs upsampling on a texture, 16 | // as taken from Call Of Duty method, presented at ACM Siggraph 2014. 17 | // This particular method was customly designed to eliminate 18 | // "pulsating artifacts and temporal stability issues". 19 | 20 | @compute @workgroup_size(16, 16, 1) 21 | fn cs(@builtin(global_invocation_id) global_id: vec3) { 22 | let target_texel_size = 1.0 / bloom_blur_constants.output_texture_size; 23 | 24 | let tex_coord = (vec2f(global_id.xy) + vec2f(0.5)) * target_texel_size; 25 | 26 | if (global_id.x < u32(bloom_blur_constants.output_texture_size.x) && 27 | global_id.y < u32(bloom_blur_constants.output_texture_size.y)) { 28 | // The filter kernel is applied with a radius, specified in texture 29 | // coordinates, so that the radius will vary across mip resolutions. 30 | let x = bloom_blur_constants.bloom_filter_radius; 31 | let y = bloom_blur_constants.bloom_filter_radius; 32 | 33 | // Take 9 samples around current texel: 34 | // a - b - c 35 | // d - e - f 36 | // g - h - i 37 | // === ('e' is the current texel) === 38 | let a = textureSampleLevel(input_texture, clamped_sampler, vec2f(tex_coord.x - x, tex_coord.y + y), 0).rgb; 39 | let b = textureSampleLevel(input_texture, clamped_sampler, vec2f(tex_coord.x, tex_coord.y + y), 0).rgb; 40 | let c = textureSampleLevel(input_texture, clamped_sampler, vec2f(tex_coord.x + x, tex_coord.y + y), 0).rgb; 41 | 42 | let d = textureSampleLevel(input_texture, clamped_sampler, vec2f(tex_coord.x - x, tex_coord.y), 0).rgb; 43 | let e = textureSampleLevel(input_texture, clamped_sampler, vec2f(tex_coord.x, tex_coord.y), 0).rgb; 44 | let f = textureSampleLevel(input_texture, clamped_sampler, vec2f(tex_coord.x + x, tex_coord.y), 0).rgb; 45 | 46 | let g = textureSampleLevel(input_texture, clamped_sampler, vec2f(tex_coord.x - x, tex_coord.y - y), 0).rgb; 47 | let h = textureSampleLevel(input_texture, clamped_sampler, vec2f(tex_coord.x, tex_coord.y - y), 0).rgb; 48 | let i = textureSampleLevel(input_texture, clamped_sampler, vec2f(tex_coord.x + x, tex_coord.y - y), 0).rgb; 49 | 50 | // Apply weighted distribution, by using a 3x3 tent filter: 51 | // 1 | 1 2 1 | 52 | // -- * | 2 4 2 | 53 | // 16 | 1 2 1 | 54 | var upsample_color = e * 4.0; 55 | upsample_color += (b + d + f + h) * 2.0; 56 | upsample_color += (a + c + g + i); 57 | upsample_color *= 1.0 / 16.0; 58 | 59 | textureStore(output_texture, vec2i(global_id.xy), vec4f(upsample_color, 1.0)); 60 | } 61 | } -------------------------------------------------------------------------------- /assets/engine/shaders/effects/crt_post.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | #include "postprocess_common.wgsl" 3 | 4 | // ------------------------------------------------------------------------------------ 5 | // Data Structures 6 | // ------------------------------------------------------------------------------------ 7 | 8 | struct CrtParams { 9 | curvature: f32, // Screen curvature amount 10 | vignette: f32, // Vignette darkness 11 | scan_brightness: f32, // Brightness of scanlines 12 | rgb_offset: f32, // RGB subpixel separation 13 | bloom_strength: f32, // Bloom/glow effect strength 14 | } 15 | 16 | struct VertexOutput { 17 | @builtin(position) position: vec4f, 18 | @location(0) uv: vec2, 19 | @location(1) @interpolate(flat) instance_index: u32, 20 | }; 21 | 22 | struct FragmentOutput { 23 | @location(0) color: vec4, 24 | }; 25 | 26 | 27 | // ------------------------------------------------------------------------------------ 28 | // Buffers 29 | // ------------------------------------------------------------------------------------ 30 | 31 | @group(1) @binding(0) var crt_params: CrtParams; 32 | @group(1) @binding(1) var input_texture: texture_2d; 33 | 34 | // ------------------------------------------------------------------------------------ 35 | // Helper Functions 36 | // ------------------------------------------------------------------------------------ 37 | 38 | // Apply screen curvature distortion 39 | fn curve_uv(uv: vec2f, curvature: f32) -> vec2f { 40 | // Convert UV to centered coordinates (-1 to 1) 41 | var curved_uv = uv * 2.0 - 1.0; 42 | 43 | // Apply barrel distortion 44 | let barrel = curved_uv * curved_uv * curved_uv; 45 | curved_uv += barrel * curvature; 46 | 47 | // Convert back to 0-1 range 48 | curved_uv = curved_uv * 0.5 + 0.5; 49 | 50 | return curved_uv; 51 | } 52 | 53 | // Create RGB subpixel pattern 54 | fn rgb_split(uv: vec2f, offset: f32) -> vec3f { 55 | let pixel_offset = offset * 0.001; 56 | let r = textureSample(input_texture, global_sampler, vec2f(uv.x + pixel_offset, uv.y)).r; 57 | let g = textureSample(input_texture, global_sampler, uv).g; 58 | let b = textureSample(input_texture, global_sampler, vec2f(uv.x - pixel_offset, uv.y)).b; 59 | return vec3f(r, g, b); 60 | } 61 | 62 | // Create scanline pattern 63 | fn scanlines(uv: vec2f, brightness: f32) -> f32 { 64 | let scan_size = 400.0; 65 | let scan = sin(uv.y * scan_size) * 0.5 + 0.5; 66 | return mix(1.0, scan, brightness); 67 | } 68 | 69 | // Vignette effect 70 | fn vignette(uv: vec2f, strength: f32) -> f32 { 71 | let center = vec2f(0.5); 72 | let dist = distance(uv, center); 73 | return 1.0 - smoothstep(0.4, 0.7, dist * strength); 74 | } 75 | 76 | // Simple bloom effect 77 | fn bloom(uv: vec2f, strength: f32) -> vec3f { 78 | let blur_size = 0.004; 79 | var bloom_color = vec3f(0.0); 80 | 81 | // 9-tap gaussian blur 82 | for (var i = -1; i <= 1; i++) { 83 | for (var j = -1; j <= 1; j++) { 84 | let offset = vec2f(f32(i), f32(j)) * blur_size; 85 | bloom_color += textureSample( 86 | input_texture, 87 | global_sampler, 88 | uv + offset 89 | ).rgb; 90 | } 91 | } 92 | 93 | bloom_color /= 9.0; 94 | return bloom_color * strength; 95 | } 96 | 97 | // ------------------------------------------------------------------------------------ 98 | // Fragment Shader 99 | // ------------------------------------------------------------------------------------ 100 | 101 | @fragment 102 | fn fs(v_out: VertexOutput) -> FragmentOutput { 103 | // Apply screen curvature 104 | var curved_uv = curve_uv(v_out.uv, crt_params.curvature); 105 | 106 | // Get base color with RGB subpixel separation 107 | var color = rgb_split(curved_uv, crt_params.rgb_offset); 108 | 109 | // Apply scanlines 110 | color *= scanlines(curved_uv, crt_params.scan_brightness); 111 | 112 | // Add bloom 113 | color += bloom(curved_uv, crt_params.bloom_strength); 114 | 115 | // Apply vignette 116 | color *= vignette(curved_uv, crt_params.vignette); 117 | 118 | // Enhance contrast and colors 119 | color = pow(color, vec3f(1.2)); // Contrast boost 120 | color *= 1.2; // Brightness boost 121 | 122 | // Add subtle color tinting to simulate phosphor colors 123 | color *= vec3f(1.0, 0.97, 0.95); // Slightly warmer 124 | 125 | return FragmentOutput(vec4f(color, 1.0)); 126 | } -------------------------------------------------------------------------------- /assets/engine/shaders/effects/outline_post.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | #include "postprocess_common.wgsl" 3 | 4 | // ------------------------------------------------------------------------------------ 5 | // Data Structures 6 | // ------------------------------------------------------------------------------------ 7 | 8 | struct OutlineParams { 9 | outline_thickness: f32, 10 | depth_threshold: f32, 11 | normal_threshold: f32, 12 | depth_scale: f32, 13 | outline_color: vec4f, 14 | } 15 | 16 | struct VertexOutput { 17 | @builtin(position) position: vec4f, 18 | @location(0) uv: vec2, 19 | @location(1) @interpolate(flat) instance_index: u32, 20 | }; 21 | 22 | struct FragmentOutput { 23 | @location(0) color: vec4, 24 | }; 25 | 26 | // ------------------------------------------------------------------------------------ 27 | // Buffers 28 | // ------------------------------------------------------------------------------------ 29 | 30 | @group(1) @binding(0) var outline_params: OutlineParams; 31 | @group(1) @binding(1) var color_texture: texture_2d; 32 | @group(1) @binding(2) var depth_texture: texture_2d; 33 | @group(1) @binding(3) var normal_texture: texture_2d; 34 | 35 | // ------------------------------------------------------------------------------------ 36 | // Helper Functions 37 | // ------------------------------------------------------------------------------------ 38 | 39 | fn sample_depth(uv: vec2f) -> f32 { 40 | return textureSample(depth_texture, non_filtering_sampler, uv).r; 41 | } 42 | 43 | fn sample_normal(uv: vec2f) -> vec3f { 44 | return textureSample(normal_texture, global_sampler, uv).xyz * 2.0 - 1.0; 45 | } 46 | 47 | fn detect_edge(uv: vec2f) -> f32 { 48 | let pixel_size = vec2f(1.0) / vec2f(textureDimensions(depth_texture)); 49 | 50 | // Sample the 4 adjacent pixels (cross pattern) 51 | let offsets = array( 52 | vec2f(1.0, 0.0), // right 53 | vec2f(-1.0, 0.0), // left 54 | vec2f(0.0, 1.0), // up 55 | vec2f(0.0, -1.0) // down 56 | ); 57 | 58 | // Sample center pixel 59 | let depth_center = sample_depth(uv); 60 | let normal_center = sample_normal(uv); 61 | 62 | var edge = 0.0; 63 | 64 | // Check each adjacent pixel 65 | for (var i = 0; i < 4; i++) { 66 | let uv_offset = uv + offsets[i] * pixel_size * outline_params.outline_thickness; 67 | 68 | // Sample neighbor 69 | let depth_sample = sample_depth(uv_offset); 70 | let normal_sample = sample_normal(uv_offset); 71 | 72 | // Compute depth difference 73 | let depth_diff = abs(depth_center - depth_sample) * outline_params.depth_scale; 74 | 75 | // Compute normal difference 76 | let normal_diff = 1.0 - max(dot(normal_center, normal_sample), 0.0); 77 | 78 | // Combine depth and normal edges 79 | let depth_edge = step(outline_params.depth_threshold, depth_diff); 80 | let normal_edge = step(outline_params.normal_threshold, normal_diff); 81 | 82 | edge = max(edge, max(depth_edge, normal_edge)); 83 | } 84 | 85 | return edge; 86 | } 87 | 88 | // ------------------------------------------------------------------------------------ 89 | // Fragment Shader 90 | // ------------------------------------------------------------------------------------ 91 | 92 | @fragment 93 | fn fs(v_out: VertexOutput) -> FragmentOutput { 94 | let uv = vec2(v_out.uv); 95 | let edge = detect_edge(uv); 96 | 97 | // Sample original color 98 | let original_color = textureSample(color_texture, global_sampler, uv); 99 | 100 | // Only show outline (no white dots) 101 | let final_color = select(original_color, outline_params.outline_color, edge > 0.1); 102 | 103 | return FragmentOutput(vec4(final_color)); 104 | } -------------------------------------------------------------------------------- /assets/engine/shaders/effects/transform_ripples.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | 3 | @group(1) @binding(0) var entity_positions: array; 4 | @group(1) @binding(1) var entity_flags: array>; 5 | 6 | @compute @workgroup_size(256) 7 | fn cs(@builtin(global_invocation_id) global_id: vec3) { 8 | let entity = global_id.x; 9 | if (entity >= arrayLength(&entity_flags)) { 10 | return; 11 | } 12 | 13 | var entity_position = entity_positions[entity]; 14 | 15 | let seed = f32(random_seed(u32(entity))) * 0.0001; 16 | let time = (frame_info.time + seed) * 2.0; 17 | 18 | entity_position.y += sin(time) * 0.02; 19 | 20 | entity_positions[entity] = entity_position; 21 | 22 | atomicOr(&entity_flags[entity], EF_TRANSFORM_DIRTY); 23 | } -------------------------------------------------------------------------------- /assets/engine/shaders/entity_prepass.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | 3 | // ------------------------------------------------------------------------------------ 4 | // Data Structures 5 | // ------------------------------------------------------------------------------------ 6 | 7 | struct VertexOutput { 8 | @builtin(position) position: vec4f, 9 | @location(0) @interpolate(flat) entity_id: u32, 10 | @location(1) @interpolate(flat) base_entity_id: u32, 11 | }; 12 | 13 | struct FragmentOutput { 14 | @location(0) entity_id: vec2, 15 | } 16 | 17 | // ------------------------------------------------------------------------------------ 18 | // Buffers 19 | // ------------------------------------------------------------------------------------ 20 | 21 | @group(1) @binding(0) var entity_transforms: array; 22 | @group(1) @binding(1) var compacted_object_instances: array; 23 | 24 | // ------------------------------------------------------------------------------------ 25 | // Vertex Shader 26 | // ------------------------------------------------------------------------------------ 27 | 28 | @vertex fn vs( 29 | @builtin(vertex_index) vi : u32, 30 | @builtin(instance_index) ii: u32 31 | ) -> VertexOutput { 32 | let entity_resolved = get_entity_row(compacted_object_instances[ii].row); 33 | 34 | let model_matrix = entity_transforms[entity_resolved].transform; 35 | let mvp = view_buffer[0].view_projection_matrix * model_matrix; 36 | 37 | var output : VertexOutput; 38 | 39 | output.position = mvp * vertex_buffer[vi].position; 40 | 41 | output.base_entity_id = entity_resolved; 42 | output.entity_id = entity_resolved; 43 | 44 | return output; 45 | } 46 | 47 | // ------------------------------------------------------------------------------------ 48 | // Fragment Shader 49 | // ------------------------------------------------------------------------------------ 50 | 51 | @fragment fn fs(v_out: VertexOutput) -> FragmentOutput { 52 | var output : FragmentOutput; 53 | 54 | output.entity_id = vec2(v_out.base_entity_id, v_out.entity_id); 55 | 56 | return output; 57 | } -------------------------------------------------------------------------------- /assets/engine/shaders/fullscreen.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | 3 | // ------------------------------------------------------------------------------------ 4 | // Buffers 5 | // ------------------------------------------------------------------------------------ 6 | 7 | @group(1) @binding(0) var input_texture: texture_2d; 8 | 9 | // ------------------------------------------------------------------------------------ 10 | // Data Structures 11 | // ------------------------------------------------------------------------------------ 12 | 13 | struct VertexOutput { 14 | @builtin(position) position: vec4f, 15 | @location(0) uv: vec2, 16 | @location(1) @interpolate(flat) instance_index: u32, 17 | }; 18 | 19 | struct FragmentOutput { 20 | @location(0) color: vec4, 21 | }; 22 | 23 | 24 | // ------------------------------------------------------------------------------------ 25 | // Vertex Shader 26 | // ------------------------------------------------------------------------------------ 27 | 28 | @vertex fn vs( 29 | @builtin(vertex_index) vi : u32, 30 | @builtin(instance_index) ii: u32 31 | ) -> VertexOutput { 32 | var output : VertexOutput; 33 | output.position = vec4(vertex_buffer[vi].position); 34 | output.uv = vertex_buffer[vi].uv; 35 | output.instance_index = ii; 36 | return output; 37 | } 38 | 39 | // ------------------------------------------------------------------------------------ 40 | // Fragment Shader 41 | // ------------------------------------------------------------------------------------ 42 | 43 | @fragment fn fs(v_out: VertexOutput) -> FragmentOutput { 44 | var color = vec4(textureSample(input_texture, global_sampler, vec2(v_out.uv))); 45 | return FragmentOutput(color); 46 | } -------------------------------------------------------------------------------- /assets/engine/shaders/hzb_reduce.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | 3 | // ------------------------------------------------------------------------------------ 4 | // Data Structures 5 | // ------------------------------------------------------------------------------------ 6 | struct HZBParams { 7 | input_image_size: vec2, 8 | output_image_size: vec2, 9 | } 10 | 11 | // ------------------------------------------------------------------------------------ 12 | // Compute Shader 13 | // ------------------------------------------------------------------------------------ 14 | 15 | @group(1) @binding(0) var input_texture: texture_2d; 16 | @group(1) @binding(1) var output_texture: texture_storage_2d; 17 | @group(1) @binding(2) var params: HZBParams; 18 | 19 | @compute @workgroup_size(16, 16, 1) 20 | fn cs(@builtin(global_invocation_id) global_id: vec3) { 21 | let output_texel_size = 1.0 / params.output_image_size; 22 | let input_texel_size = 1.0 / params.input_image_size; 23 | 24 | let output_width = params.output_image_size.x; 25 | let output_height = params.output_image_size.y; 26 | 27 | if (global_id.x >= u32(output_width) || global_id.y >= u32(output_height)) { 28 | return; 29 | } 30 | 31 | let base_uv = (vec2f(global_id.xy) + vec2f(0.5)) * output_texel_size; 32 | 33 | var depth = textureSampleLevel(input_texture, non_filtering_sampler, base_uv, 0).r; 34 | 35 | textureStore(output_texture, global_id.xy, vec4(depth)); 36 | } -------------------------------------------------------------------------------- /assets/engine/shaders/line.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | 3 | struct VertexInput { 4 | @builtin(vertex_index) vertex_index : u32, 5 | @builtin(instance_index) instance_index: u32, 6 | }; 7 | 8 | struct VertexOutput { 9 | @builtin(position) position: vec4, 10 | @location(0) color: vec4, 11 | @location(1) uv: vec2, 12 | }; 13 | 14 | struct FragmentOutput { 15 | @location(0) color: vec4, 16 | @location(1) emissive: vec4, 17 | @location(2) smra: vec4, 18 | @location(3) position: vec4, 19 | @location(4) normal: vec4, 20 | } 21 | 22 | struct TransformData { 23 | transform: mat4x4f, 24 | }; 25 | 26 | struct LineData { 27 | color_and_width: vec4f, 28 | }; 29 | 30 | @group(1) @binding(0) var transform_data: array; 31 | @group(1) @binding(1) var line_data: array; 32 | 33 | @vertex 34 | fn vs(input: VertexInput) -> VertexOutput { 35 | var output: VertexOutput; 36 | 37 | let vertex = vertex_buffer[input.vertex_index]; 38 | let color_and_width = line_data[input.instance_index].color_and_width; 39 | let model_transform = transform_data[input.instance_index].transform; 40 | 41 | let model_view_transform = view_buffer[0].view_matrix * model_transform; 42 | 43 | // Calculate line direction in view space 44 | let view_start = (model_view_transform * vec4(0.0, 0.0, 0.0, 1.0)).xyz; 45 | let view_end = (model_view_transform * vec4(1.0, 0.0, 0.0, 1.0)).xyz; 46 | let line_dir = normalize(view_end - view_start); 47 | 48 | // Calculate camera-facing perpendicular direction 49 | // This ensures consistent width regardless of viewing angle 50 | let view_dir = normalize(view_start); // Direction from camera to line start 51 | var perp_dir = normalize(cross(line_dir, view_dir)); 52 | 53 | // If line_dir and view_dir are parallel, use a different vector 54 | if (length(perp_dir) < 0.001) { 55 | let alt_up = vec3(0.0, 1.0, 0.0); 56 | perp_dir = normalize(cross(line_dir, alt_up)); 57 | } 58 | 59 | // The quad vertices are at UV coordinates (0,0), (1,0), (0,1), (1,1) 60 | let uv = vertex.uv; 61 | 62 | // Determine position along the line (x-coordinate of UV) 63 | let pos_along_line = mix(view_start, view_end, uv.x); 64 | 65 | // Determine offset from line center (y-coordinate of UV, remapped from [0,1] to [-0.5,0.5]) 66 | let offset = perp_dir * (uv.y - 0.5) * color_and_width.w; 67 | 68 | // Final position in view space 69 | let final_view_pos = pos_along_line + offset; 70 | 71 | // Convert directly to clip space from view space 72 | output.position = view_buffer[0].projection_matrix * vec4(final_view_pos, 1.0); 73 | 74 | // Pass color and UV to fragment shader 75 | output.color = vec4(color_and_width.x, color_and_width.y, color_and_width.z, 1.0); 76 | output.uv = uv; 77 | 78 | return output; 79 | } 80 | 81 | @fragment 82 | fn fs(input: VertexOutput) -> FragmentOutput { 83 | var output: FragmentOutput; 84 | 85 | // Calculate distance from center line for anti-aliasing 86 | let distance_from_center = abs(input.uv.y - 0.5) * 2.0; // Range 0 to 1 87 | 88 | // Discard pixels that are too far from the center 89 | if (distance_from_center > 0.95) { 90 | discard; 91 | } 92 | 93 | output.color = input.color; 94 | output.position = input.position; 95 | output.emissive = vec4f(2.0, 0.0, 0.0, 0.0); 96 | //output.smra = vec4f(0.0, 0.0, 0.0, 0.0); 97 | output.normal = vec4f(0.0, 0.0, 0.0, 0.0); 98 | 99 | return output; 100 | } -------------------------------------------------------------------------------- /assets/engine/shaders/postprocess_common.wgsl: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------------ 2 | // Tonemapping 3 | // ------------------------------------------------------------------------------------ 4 | fn reinhard_tonemapping(color: vec3, exposure: f32) -> vec3 { 5 | var adjusted_color = color * exposure; 6 | return adjusted_color / (adjusted_color + vec3(1.0)); 7 | } 8 | 9 | fn u2_filmic_tonemapping(color: vec3, exposure: f32) -> vec3 { 10 | var adjusted_color = color * exposure; 11 | 12 | let A: f32 = 0.15; 13 | let B: f32 = 0.50; 14 | let C: f32 = 0.10; 15 | let D: f32 = 0.20; 16 | let E: f32 = 0.02; 17 | let F: f32 = 0.30; 18 | 19 | return ((adjusted_color * (A * adjusted_color + vec3(C * B)) + vec3(D * E)) / (adjusted_color * (A * adjusted_color + vec3(B)) + vec3(D * F))) - vec3(E / F); 20 | } 21 | 22 | fn aces_filmic_tonemapping(x: vec3) -> vec3 { 23 | let a: f32 = 2.51; 24 | let b: f32 = 0.03; 25 | let c: f32 = 2.43; 26 | let d: f32 = 0.59; 27 | let e: f32 = 0.14; 28 | return (x * (a * x + b)) / (x * (c * x + d) + e); 29 | } 30 | 31 | fn aces_tonemapping(color: vec3, exposure: f32) -> vec3 { 32 | var adjusted_color = color * exposure; 33 | adjusted_color = aces_filmic_tonemapping(adjusted_color); 34 | return adjusted_color; 35 | } -------------------------------------------------------------------------------- /assets/engine/shaders/skybox.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | 3 | struct SkyboxData { 4 | @location(0) color: vec4, 5 | }; 6 | 7 | @group(1) @binding(0) var skybox_texture: texture_cube; 8 | @group(1) @binding(1) var skybox_data: SkyboxData; 9 | 10 | // ------------------------------------------------------------------------------------ 11 | // Data Structures 12 | 13 | // ------------------------------------------------------------------------------------ 14 | struct VertexOutput { 15 | @builtin(position) position: vec4f, 16 | @location(0) pos: vec4f, 17 | }; 18 | 19 | struct FragmentOutput { 20 | @location(0) color: vec4, 21 | }; 22 | 23 | // ------------------------------------------------------------------------------------ 24 | // Vertex Shader 25 | // ------------------------------------------------------------------------------------ 26 | @vertex fn vs( 27 | @builtin(vertex_index) vi : u32, 28 | @builtin(instance_index) ii: u32 29 | ) -> VertexOutput { 30 | var output : VertexOutput; 31 | 32 | // Extract the rotation part of the view matrix (3x3 upper-left part) 33 | var rotation_view = mat3x3f( 34 | view_buffer[0].view_matrix[0].xyz, 35 | view_buffer[0].view_matrix[1].xyz, 36 | view_buffer[0].view_matrix[2].xyz 37 | ); 38 | 39 | // Apply rotation to the vertex position 40 | var rotated_pos = rotation_view * vec3(vertex_buffer[vi].position.xyz); 41 | 42 | // Apply projection matrix 43 | output.position = view_buffer[0].projection_matrix * vec4f(rotated_pos, 1.0); 44 | 45 | // Store the rotated position for fragment shader 46 | output.pos = 0.5 * (vec4(vertex_buffer[vi].position) + vec4(1.0, 1.0, 1.0, 1.0)); 47 | 48 | return output; 49 | } 50 | 51 | // ------------------------------------------------------------------------------------ 52 | // Fragment Shader 53 | // ------------------------------------------------------------------------------------ 54 | @fragment fn fs(v_out: VertexOutput) -> FragmentOutput { 55 | var dir = v_out.pos.xyz - vec3(0.5); 56 | dir.z *= -1; 57 | var color = textureSample(skybox_texture, global_sampler, dir) * skybox_data.color; 58 | return FragmentOutput(vec4(color)); 59 | } -------------------------------------------------------------------------------- /assets/engine/shaders/standard_material.wgsl: -------------------------------------------------------------------------------- 1 | #define CUSTOM_FS 2 | 3 | #include "gbuffer_base.wgsl" 4 | 5 | struct MaterialParams { 6 | albedo: vec4, 7 | normal: vec4, 8 | emission_roughness_metallic_tiling: vec4, 9 | ao_height_specular: vec4, 10 | texture_flags1: vec4, // x: albedo, y: normal, z: roughness, w: metallic 11 | texture_flags2: vec4, // x: ao, y: height, z: specular, w: emission 12 | } 13 | 14 | @group(2) @binding(0) var material_params: MaterialParams; 15 | @group(2) @binding(1) var albedo: texture_2d; 16 | @group(2) @binding(2) var normal: texture_2d; 17 | @group(2) @binding(3) var roughness: texture_2d; 18 | @group(2) @binding(4) var metallic: texture_2d; 19 | @group(2) @binding(5) var emission: texture_2d; 20 | @group(2) @binding(6) var ao: texture_2d; 21 | 22 | fn sample_texture_or_vec4_param( 23 | texture: texture_2d, 24 | uv: vec2, 25 | param: vec4, 26 | use_texture: u32 27 | ) -> vec4 { 28 | if (use_texture != 0u) { 29 | return textureSample(texture, global_sampler, uv); 30 | } 31 | return param; 32 | } 33 | 34 | fn sample_texture_or_float_param( 35 | texture: texture_2d, 36 | uv: vec2, 37 | param: precision_float, 38 | use_texture: u32 39 | ) -> precision_float { 40 | if (use_texture != 0u) { 41 | return textureSample(texture, global_sampler, uv).r; 42 | } 43 | return param; 44 | } 45 | 46 | // ------------------------------------------------------------------------------------ 47 | // Fragment Shader 48 | // ------------------------------------------------------------------------------------ 49 | fn fragment(v_out: VertexOutput, f_out: ptr) -> FragmentOutput { 50 | let tiling = material_params.emission_roughness_metallic_tiling.w; 51 | var uv = v_out.uv * tiling; 52 | 53 | let albedo = sample_texture_or_vec4_param( 54 | albedo, 55 | uv, 56 | material_params.albedo, 57 | material_params.texture_flags1.x 58 | ); 59 | let roughness = sample_texture_or_float_param( 60 | roughness, 61 | uv, 62 | material_params.emission_roughness_metallic_tiling.y, 63 | material_params.texture_flags1.z 64 | ); 65 | let metallic = sample_texture_or_float_param( 66 | metallic, 67 | uv, 68 | material_params.emission_roughness_metallic_tiling.z, 69 | material_params.texture_flags1.w 70 | ); 71 | let ao = sample_texture_or_float_param( 72 | ao, 73 | uv, 74 | material_params.ao_height_specular.x, 75 | material_params.texture_flags2.x 76 | ); 77 | let emissive = sample_texture_or_float_param( 78 | emission, 79 | uv, 80 | material_params.emission_roughness_metallic_tiling.x, 81 | material_params.texture_flags2.w 82 | ); 83 | 84 | // Apply normal mapping if enabled 85 | if (material_params.texture_flags1.y != 0u) { 86 | let tbn_matrix = mat3x3( 87 | v_out.tangent.xyz, 88 | v_out.bitangent.xyz, 89 | v_out.normal.xyz 90 | ); 91 | let normal_map = get_normal_from_normal_map( 92 | normal, 93 | uv, 94 | tbn_matrix 95 | ); 96 | f_out.normal = vec4(normal_map, 1.0); 97 | } 98 | 99 | f_out.albedo = albedo; 100 | f_out.smra.r = material_params.ao_height_specular.z; 101 | f_out.smra.g = roughness; 102 | f_out.smra.b = metallic; 103 | f_out.smra.a = ao; 104 | f_out.emissive.r = emissive; 105 | 106 | return *f_out; 107 | } -------------------------------------------------------------------------------- /assets/engine/shaders/system_compute/bounds_processing.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | 3 | // ------------------------------------------------------------------------------------ 4 | // Constants 5 | // ------------------------------------------------------------------------------------ 6 | 7 | const bounds_padding = 1.0; 8 | 9 | // ------------------------------------------------------------------------------------ 10 | // Buffers 11 | // ------------------------------------------------------------------------------------ 12 | 13 | @group(1) @binding(0) var entity_transforms: array; 14 | @group(1) @binding(1) var entity_flags: array; 15 | @group(1) @binding(2) var aabb_bounds: array; 16 | @group(1) @binding(3) var entity_aabb_node_indices: array; 17 | 18 | // ------------------------------------------------------------------------------------ 19 | // Compute Shader 20 | // ------------------------------------------------------------------------------------ 21 | 22 | @compute @workgroup_size(256) 23 | fn cs(@builtin(global_invocation_id) global_id: vec3) { 24 | if (global_id.x >= arrayLength(&entity_aabb_node_indices)) { 25 | return; 26 | } 27 | 28 | let entity_id_offset = get_entity_row(global_id.x); 29 | let node_index = entity_aabb_node_indices[entity_id_offset]; 30 | 31 | // if there's no valid AABB node, skip processing entirely 32 | if (node_index == 0u) { 33 | return; 34 | } 35 | 36 | // Get the entity's world transform 37 | let transform = entity_transforms[entity_id_offset].transform; 38 | let position = transform[3].xyz; 39 | let scale = vec3f(length(transform[0].xyz), length(transform[1].xyz), length(transform[2].xyz)); 40 | 41 | // Calculate bounds based on position and scale 42 | // This is a simple axis-aligned box, but could be more sophisticated 43 | // based on the entity's mesh or collider 44 | let half_size = vec3f( 45 | abs(scale[0]) * 0.5, 46 | abs(scale[1]) * 0.5, 47 | abs(scale[2]) * 0.5, 48 | ); 49 | 50 | // Add padding 51 | let padding = vec3f( 52 | half_size[0] * bounds_padding, 53 | half_size[1] * bounds_padding, 54 | half_size[2] * bounds_padding, 55 | ); 56 | 57 | let min_point = vec3f( 58 | position[0] - half_size[0] - padding[0], 59 | position[1] - half_size[1] - padding[1], 60 | position[2] - half_size[2] - padding[2], 61 | ); 62 | 63 | let max_point = vec3f( 64 | position[0] + half_size[0] + padding[0], 65 | position[1] + half_size[1] + padding[1], 66 | position[2] + half_size[2] + padding[2], 67 | ); 68 | 69 | // write to full array for other GPU consumers 70 | aabb_bounds[node_index].min_point = vec4f(min_point, 1.0); 71 | aabb_bounds[node_index].max_point = vec4f(max_point, 1.0); 72 | 73 | entity_flags[entity_id_offset] |= EF_AABB_DIRTY; 74 | } -------------------------------------------------------------------------------- /assets/engine/shaders/system_compute/clear_dirty_flags.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | 3 | @group(1) @binding(0) var flags_meta: array>; 4 | 5 | @compute @workgroup_size(128) 6 | fn cs(@builtin(global_invocation_id) global_id: vec3) { 7 | let entity_index = global_id.x; 8 | 9 | if (entity_index >= arrayLength(&flags_meta)) { 10 | return; 11 | } 12 | 13 | atomicAnd(&flags_meta[entity_index], ~EF_DIRTY); 14 | } -------------------------------------------------------------------------------- /assets/engine/shaders/system_compute/compact_lights.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | #include "lighting_common.wgsl" 3 | 4 | @group(1) @binding(0) var lights_buffer: array; 5 | @group(1) @binding(1) var dense_lights_buffer: array; 6 | @group(1) @binding(2) var light_count_buffer: array>; 7 | 8 | @compute @workgroup_size(128) 9 | fn cs(@builtin(global_invocation_id) global_id: vec3) { 10 | let idx = global_id.x; 11 | if (idx >= arrayLength(&lights_buffer)) { 12 | return; 13 | } 14 | let light = lights_buffer[idx]; 15 | if (light.activated > 0.0) { 16 | let dst = atomicAdd(&light_count_buffer[0], 1u); 17 | dense_lights_buffer[dst] = light; 18 | } 19 | } -------------------------------------------------------------------------------- /assets/engine/shaders/system_compute/line_transform_processing.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | 3 | // Line transform processing compute shader 4 | // Calculates transform matrices for lines based on start and end positions 5 | 6 | // Define the line position struct 7 | struct LinePosition { 8 | start: vec4f, 9 | end: vec4f, 10 | } 11 | 12 | // Define the binding groups 13 | @group(1) @binding(0) var transforms: array; 14 | @group(1) @binding(1) var line_positions: array; 15 | 16 | fn mat4_identity() -> mat4x4f { 17 | return mat4x4f( 18 | vec4f(1.0, 0.0, 0.0, 0.0), 19 | vec4f(0.0, 1.0, 0.0, 0.0), 20 | vec4f(0.0, 0.0, 1.0, 0.0), 21 | vec4f(0.0, 0.0, 0.0, 1.0) 22 | ); 23 | } 24 | 25 | fn mat4_from_scaling(scale: vec3f) -> mat4x4f { 26 | return mat4x4f( 27 | vec4f(scale.x, 0.0, 0.0, 0.0), 28 | vec4f(0.0, scale.y, 0.0, 0.0), 29 | vec4f(0.0, 0.0, scale.z, 0.0), 30 | vec4f(0.0, 0.0, 0.0, 1.0) 31 | ); 32 | } 33 | 34 | // Main function to create a line transform 35 | fn create_line_transform(start: vec4f, end: vec4f) -> mat4x4f { 36 | // Calculate direction vector from start to end 37 | let direction = end.xyz - start.xyz; 38 | 39 | // Calculate length of the line 40 | let line_length = length(direction); 41 | 42 | if (line_length < 0.0001) { 43 | // Handle degenerate case (zero-length line) 44 | return mat4_identity(); 45 | } 46 | 47 | // Normalized direction 48 | let normalized_direction = direction / line_length; 49 | 50 | // Find perpendicular vectors to create a coordinate system 51 | // Start with a default up vector 52 | var up = vec3f(0.0, 1.0, 0.0); 53 | 54 | // If direction is too close to up, use a different reference vector 55 | if (abs(dot(normalized_direction, up)) > 0.99) { 56 | up = vec3f(0.0, 0.0, 1.0); 57 | } 58 | 59 | // Calculate right vector (perpendicular to both direction and up) 60 | let right = normalize(cross(normalized_direction, up)); 61 | 62 | // Recalculate a true up vector that's perpendicular to both direction and right 63 | let true_up = normalize(cross(right, normalized_direction)); 64 | 65 | // Build rotation matrix (column-major) 66 | // The columns represent the transformed basis vectors 67 | var rotation_matrix = mat4x4f( 68 | vec4f(normalized_direction, 0.0), // x-axis maps to direction 69 | vec4f(true_up, 0.0), // y-axis maps to up 70 | vec4f(right, 0.0), // z-axis maps to right 71 | vec4f(0.0, 0.0, 0.0, 1.0) // no translation in rotation matrix 72 | ); 73 | 74 | // Create translation matrix to position at start point 75 | let translation_matrix = mat4x4f( 76 | vec4f(1.0, 0.0, 0.0, 0.0), 77 | vec4f(0.0, 1.0, 0.0, 0.0), 78 | vec4f(0.0, 0.0, 1.0, 0.0), 79 | vec4f(start.xyz, 1.0) 80 | ); 81 | 82 | // Apply scale to make the line the correct length 83 | let scale_matrix = mat4_from_scaling(vec3f(line_length, 1.0, 1.0)); 84 | 85 | // Combine transformations: first scale, then rotate, then translate 86 | // Order matters in matrix multiplication 87 | return translation_matrix * rotation_matrix * scale_matrix; 88 | } 89 | 90 | // Compute shader entry point 91 | @compute @workgroup_size(64) 92 | fn cs(@builtin(global_invocation_id) global_id: vec3u) { 93 | let index = global_id.x; 94 | 95 | // Make sure we don't go out of bounds 96 | if (index >= arrayLength(&line_positions)) { 97 | return; 98 | } 99 | 100 | // Get the line position 101 | let line_pos = line_positions[index]; 102 | 103 | let line_transform = create_line_transform(line_pos.start, line_pos.end); 104 | 105 | // Calculate the transform 106 | transforms[index] = line_transform; 107 | } -------------------------------------------------------------------------------- /assets/engine/shaders/system_compute/particle_render.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | 3 | // Particle render: writes each particle as a point into color and depth storage textures 4 | // Assumes positions are in normalized device coordinates (x,y ∈ [-1,1], z ∈ [0,1]) 5 | 6 | @group(1) @binding(0) var positions: array; 7 | @group(1) @binding(1) var indices: array; 8 | @group(1) @binding(2) var out_color: texture_storage_2d; 9 | @group(1) @binding(3) var in_depth: texture_2d; 10 | 11 | @compute @workgroup_size(256) 12 | fn cs(@builtin(global_invocation_id) global_id: vec3) { 13 | let i: u32 = global_id.x; 14 | if (i >= arrayLength(&indices)) { 15 | return; 16 | } 17 | 18 | let pid = indices[i]; 19 | let clip = positions[pid]; // clip.xyz in NDC, clip.w unused 20 | let ndc = clip.xyz; 21 | 22 | // Map from NDC [-1,1] to texture coords [0, width) / [0, height) 23 | let dims = textureDimensions(out_color); 24 | let uv = (ndc.xy * 0.5 + vec2f(0.5, 0.5)) * vec2f(f32(dims.x), f32(dims.y)); 25 | 26 | // coord is vec2 of pixel indices 27 | let d = textureLoad(in_depth, vec2(uv), 0).r; 28 | 29 | // Perform depth test: only write if current particle is closer 30 | if (ndc.z >= d) { 31 | return; 32 | } 33 | 34 | // Simple color: orange 35 | textureStore(out_color, vec2(uv), vec4f(1.0, 0.5, 0.0, 1.0)); 36 | } -------------------------------------------------------------------------------- /assets/engine/shaders/text_material.wgsl: -------------------------------------------------------------------------------- 1 | #define CUSTOM_FS 2 | #define CUSTOM_VS 3 | 4 | #include "gbuffer_base.wgsl" 5 | 6 | //------------------------------------------------------------------------------------ 7 | // Data Structures 8 | //------------------------------------------------------------------------------------ 9 | struct StringData { 10 | text_color: vec4, 11 | page_texture_size: vec2, 12 | text_emissive: precision_float, 13 | }; 14 | 15 | struct GlyphData { 16 | width: u32, 17 | height: u32, 18 | x: i32, 19 | y: i32, 20 | }; 21 | 22 | //------------------------------------------------------------------------------------ 23 | // Buffers / Textures 24 | //------------------------------------------------------------------------------------ 25 | @group(2) @binding(0) var text: array; 26 | @group(2) @binding(1) var string_data: array; 27 | @group(2) @binding(2) var font_glyph_data: array; 28 | @group(2) @binding(3) var font_page_texture: texture_2d; 29 | 30 | //------------------------------------------------------------------------------------ 31 | // VERTEX STAGE 32 | // We assume each glyph is drawn with 4 vertices (2 triangles), so we derive 33 | // the corner from `vertex_index & 3` and place it accordingly. 34 | // 35 | // corner_id | offset (in normalized quad space) 36 | // 0 | (0,0) 37 | // 1 | (1,0) 38 | // 2 | (0,1) 39 | // 3 | (1,1) 40 | //------------------------------------------------------------------------------------ 41 | fn vertex(v_out: ptr) -> VertexOutput { 42 | let entity_row = v_out.instance_id; 43 | 44 | // Find which character in the text we are drawing 45 | let string = string_data[entity_row]; 46 | let ch = text[entity_row]; 47 | let glyph_data = font_glyph_data[ch]; 48 | 49 | // Use the provided UV coordinates for corner offset 50 | var corner_offset = v_out.uv.xy; 51 | corner_offset.y = 1.0 - corner_offset.y; 52 | 53 | // Calculate UV coordinates for the glyph in the texture atlas 54 | var uv_top_left = vec2( 55 | precision_float(glyph_data.x), 56 | precision_float(glyph_data.y) 57 | ) / string.page_texture_size; 58 | 59 | let uv_size = vec2( 60 | precision_float(glyph_data.width), 61 | precision_float(glyph_data.height) 62 | ) / string.page_texture_size; 63 | 64 | // Flip Y coordinate and apply the corner offset 65 | uv_top_left.y = 1.0 - uv_top_left.y - uv_size.y; 66 | v_out.uv = uv_top_left + corner_offset * uv_size; 67 | 68 | return *v_out; 69 | } 70 | 71 | //------------------------------------------------------------------------------------ 72 | // FRAGMENT STAGE 73 | // This is where we do the MSDF decoding. We take the median of the R, G, and B channels, 74 | // offset it by 0.5 (the boundary), and use smoothstep/fwidth for anti-aliasing. 75 | // 76 | // We store the final color/alpha in f_out.albedo (and .a), though you can customize 77 | // how you blend or store this in the g-buffer pipeline. 78 | //------------------------------------------------------------------------------------ 79 | fn fragment(v_out: VertexOutput, f_out: ptr) -> FragmentOutput { 80 | // Sample the MSDF texture 81 | let entity_row = v_out.instance_id; 82 | 83 | let sample_color = vec4(textureSample(font_page_texture, global_sampler, vec2(v_out.uv))); 84 | let string_color = string_data[entity_row].text_color; 85 | let emissive = string_data[entity_row].text_emissive; 86 | 87 | let r = sample_color.r; 88 | let g = sample_color.g; 89 | let b = sample_color.b; 90 | 91 | // Compute MSDF distance 92 | let dist = median3(r, g, b); 93 | // Shift by 0.5 so that the isocontour for the glyph edge is at 0.5 94 | let sd = f32(dist) - 0.5; 95 | 96 | // fwidth() calculates how quickly 'sd' changes across the pixel, 97 | // and we use this to create a smooth transition. 98 | let w = fwidth(sd); 99 | 100 | // Anti-aliased alpha 101 | let alpha = smoothstep(-w, w, sd); 102 | if (alpha <= 0.0) { 103 | discard; 104 | } 105 | 106 | f_out.albedo = vec4(string_color.rgb, precision_float(alpha)); 107 | f_out.emissive = vec4(emissive, emissive, emissive, 0.0); 108 | 109 | 110 | f_out.smra.r = 2555.0; 111 | f_out.smra.g = 0.5; 112 | f_out.smra.b = 0.3; 113 | f_out.smra.a = 0.0; 114 | 115 | return *f_out; 116 | } 117 | -------------------------------------------------------------------------------- /assets/engine/shaders/transparency_composite.wgsl: -------------------------------------------------------------------------------- 1 | #include "common.wgsl" 2 | 3 | @group(1) @binding(0) var accumulation_texture: texture_2d; 4 | @group(1) @binding(1) var reveal_texture: texture_2d; 5 | 6 | // ------------------------------------------------------------------------------------ 7 | // Data Structures 8 | // ------------------------------------------------------------------------------------ 9 | struct VertexOutput { 10 | @builtin(position) position: vec4f, 11 | @location(0) uv: vec2, 12 | @location(1) @interpolate(flat) instance_index: u32, 13 | }; 14 | 15 | struct FragmentOutput { 16 | @location(0) color: vec4, 17 | }; 18 | 19 | 20 | // ------------------------------------------------------------------------------------ 21 | // Vertex Shader 22 | // ------------------------------------------------------------------------------------ 23 | @vertex fn vs( 24 | @builtin(vertex_index) vi : u32, 25 | @builtin(instance_index) ii: u32 26 | ) -> VertexOutput { 27 | var output : VertexOutput; 28 | output.position = vec4(vertex_buffer[vi].position); 29 | output.uv = vertex_buffer[vi].uv; 30 | output.instance_index = ii; 31 | return output; 32 | } 33 | 34 | // ------------------------------------------------------------------------------------ 35 | // Fragment Shader 36 | // ------------------------------------------------------------------------------------ 37 | @fragment fn fs(v_out: VertexOutput) -> FragmentOutput { 38 | var reveal = vec4(textureSample(reveal_texture, global_sampler, vec2(v_out.uv))); 39 | var accum = vec4(textureSample(accumulation_texture, global_sampler, vec2(v_out.uv))); 40 | var average_color = accum.rgb / max(accum.a, epsilon); 41 | return FragmentOutput(vec4(average_color, reveal.r)); 42 | 43 | } -------------------------------------------------------------------------------- /assets/engine/shaders/ui_standard_material.wgsl: -------------------------------------------------------------------------------- 1 | #define CUSTOM_FS 2 | #define TRANSPARENT 3 | 4 | #include "gbuffer_base.wgsl" 5 | 6 | 7 | //------------------------------------------------------------------------------------ 8 | // Data Structures 9 | //------------------------------------------------------------------------------------ 10 | struct ElementData { 11 | element_color: vec4f, 12 | element_emissive: f32, 13 | element_rounding: f32, 14 | }; 15 | 16 | //------------------------------------------------------------------------------------ 17 | // Buffers / Textures 18 | //------------------------------------------------------------------------------------ 19 | @group(2) @binding(0) var element_data: array; 20 | 21 | //------------------------------------------------------------------------------------ 22 | // Fragment Shader 23 | //------------------------------------------------------------------------------------ 24 | fn fragment(v_out: VertexOutput, f_out: ptr) -> FragmentOutput { 25 | let element_rounding = element_data[f_out.entity_id.y].element_rounding; 26 | let element_emissive = element_data[f_out.entity_id.y].element_emissive; 27 | var element_color = element_data[f_out.entity_id.y].element_color; 28 | 29 | // Calculate distance from edges 30 | let uv = v_out.uv; 31 | let dx = min(uv.x, 1.0 - uv.x); 32 | let dy = min(uv.y, 1.0 - uv.y); 33 | 34 | // Calculate corner distance in normalized space 35 | let corner_distance = length(vec2f( 36 | max(0.0, element_rounding - dx), 37 | max(0.0, element_rounding - dy) 38 | )); 39 | 40 | // Apply smoothed alpha based on corner distance 41 | element_color.a *= 1.0 - smoothstep(0.0, element_rounding, corner_distance); 42 | 43 | f_out.albedo = element_color; 44 | f_out.emissive = vec4f(element_emissive, element_emissive, element_emissive, 0.0); 45 | 46 | return *f_out; 47 | } -------------------------------------------------------------------------------- /assets/engine/sprites/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/sprites/cursor.png -------------------------------------------------------------------------------- /assets/engine/textures/gradientbox/nx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/gradientbox/nx.png -------------------------------------------------------------------------------- /assets/engine/textures/gradientbox/ny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/gradientbox/ny.png -------------------------------------------------------------------------------- /assets/engine/textures/gradientbox/nz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/gradientbox/nz.png -------------------------------------------------------------------------------- /assets/engine/textures/gradientbox/px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/gradientbox/px.png -------------------------------------------------------------------------------- /assets/engine/textures/gradientbox/py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/gradientbox/py.png -------------------------------------------------------------------------------- /assets/engine/textures/gradientbox/pz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/gradientbox/pz.png -------------------------------------------------------------------------------- /assets/engine/textures/simple_skybox/nx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/simple_skybox/nx.png -------------------------------------------------------------------------------- /assets/engine/textures/simple_skybox/ny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/simple_skybox/ny.png -------------------------------------------------------------------------------- /assets/engine/textures/simple_skybox/nz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/simple_skybox/nz.png -------------------------------------------------------------------------------- /assets/engine/textures/simple_skybox/px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/simple_skybox/px.png -------------------------------------------------------------------------------- /assets/engine/textures/simple_skybox/py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/simple_skybox/py.png -------------------------------------------------------------------------------- /assets/engine/textures/simple_skybox/pz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/simple_skybox/pz.png -------------------------------------------------------------------------------- /assets/engine/textures/spacebox/nx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/spacebox/nx.png -------------------------------------------------------------------------------- /assets/engine/textures/spacebox/ny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/spacebox/ny.png -------------------------------------------------------------------------------- /assets/engine/textures/spacebox/nz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/spacebox/nz.png -------------------------------------------------------------------------------- /assets/engine/textures/spacebox/px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/spacebox/px.png -------------------------------------------------------------------------------- /assets/engine/textures/spacebox/py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/spacebox/py.png -------------------------------------------------------------------------------- /assets/engine/textures/spacebox/pz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/spacebox/pz.png -------------------------------------------------------------------------------- /assets/engine/textures/voxel/dirt_albedo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/voxel/dirt_albedo.jpg -------------------------------------------------------------------------------- /assets/engine/textures/voxel/dirt_roughness.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/voxel/dirt_roughness.jpg -------------------------------------------------------------------------------- /assets/engine/textures/wall/wall_albedo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/wall/wall_albedo.png -------------------------------------------------------------------------------- /assets/engine/textures/wall/wall_ao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/wall/wall_ao.png -------------------------------------------------------------------------------- /assets/engine/textures/wall/wall_height.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/wall/wall_height.png -------------------------------------------------------------------------------- /assets/engine/textures/wall/wall_metallic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/wall/wall_metallic.png -------------------------------------------------------------------------------- /assets/engine/textures/wall/wall_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/wall/wall_normal.png -------------------------------------------------------------------------------- /assets/engine/textures/wall/wall_roughness.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/wall/wall_roughness.png -------------------------------------------------------------------------------- /assets/engine/textures/worn_panel/worn_panel_albedo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/worn_panel/worn_panel_albedo.png -------------------------------------------------------------------------------- /assets/engine/textures/worn_panel/worn_panel_ao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/worn_panel/worn_panel_ao.png -------------------------------------------------------------------------------- /assets/engine/textures/worn_panel/worn_panel_height.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/worn_panel/worn_panel_height.png -------------------------------------------------------------------------------- /assets/engine/textures/worn_panel/worn_panel_metallic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/worn_panel/worn_panel_metallic.png -------------------------------------------------------------------------------- /assets/engine/textures/worn_panel/worn_panel_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/worn_panel/worn_panel_normal.png -------------------------------------------------------------------------------- /assets/engine/textures/worn_panel/worn_panel_roughness.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/assets/engine/textures/worn_panel/worn_panel_roughness.png -------------------------------------------------------------------------------- /electron.vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | 3 | const all_config = { 4 | main: { 5 | build: { 6 | outDir: "./dist/electron/main", 7 | rollupOptions: { 8 | input: { 9 | index: "./electron/main/main.js", 10 | }, 11 | }, 12 | }, 13 | }, 14 | renderer: { 15 | root: "./dist", 16 | build: { 17 | outDir: "./dist/electron/renderer", 18 | rollupOptions: { 19 | input: "./dist/index.html", 20 | }, 21 | }, 22 | }, 23 | preload: { 24 | build: { 25 | outDir: "./dist/electron/preload", 26 | rollupOptions: { 27 | input: { 28 | index: "./electron/preload/preload.js", 29 | }, 30 | }, 31 | }, 32 | }, 33 | } 34 | 35 | export default defineConfig(all_config); 36 | -------------------------------------------------------------------------------- /electron/main/main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, session, ipcMain } = require('electron'); 2 | const path = require('path'); 3 | const fs = require('fs/promises'); 4 | 5 | const CONFIG_BASE_PATH = path.join(app.getAppPath(), 'assets/config'); 6 | 7 | const is_dev = !app.isPackaged; 8 | 9 | ipcMain.handle('config:get-config', async (event, file_name) => { 10 | try { 11 | const filePath = path.join(CONFIG_BASE_PATH, `${file_name}.json`); 12 | const data = await fs.readFile(filePath, 'utf8'); 13 | return JSON.parse(data); 14 | } catch (err) { 15 | if (err.code === 'ENOENT') return null; 16 | throw err; 17 | } 18 | }); 19 | 20 | ipcMain.handle('config:set-config', async (event, file_name, config) => { 21 | try { 22 | const filePath = path.join(CONFIG_BASE_PATH, `${file_name}.json`); 23 | await fs.writeFile(filePath, JSON.stringify(config, null, 2), 'utf8'); 24 | return { success: true }; 25 | } catch (err) { 26 | return { success: false, error: err.message }; 27 | } 28 | }); 29 | 30 | async function create_window() { 31 | const win = new BrowserWindow({ 32 | width: 1920, 33 | height: 1080, 34 | autoHideMenuBar: true, 35 | webPreferences: { 36 | nodeIntegration: true, 37 | contextIsolation: true, 38 | preload: path.join(__dirname, '../preload/index.mjs'), 39 | } 40 | }); 41 | 42 | // Enable SharedArrayBuffer 43 | session.defaultSession.webRequest.onHeadersReceived((details, callback) => { 44 | details.responseHeaders['Cross-Origin-Opener-Policy'] = ['same-origin']; 45 | details.responseHeaders['Cross-Origin-Embedder-Policy'] = ['require-corp']; 46 | callback({ responseHeaders: details.responseHeaders }); 47 | }); 48 | 49 | if (is_dev) { 50 | // Load webgpu extension in third-party directory 51 | const extension_path = path.join(app.getAppPath(), 'electron/third_party/webgpu_inspector'); 52 | await session.defaultSession.loadExtension(extension_path, { allowFileAccess: true }); 53 | win.loadFile(path.join(app.getAppPath(), 'dist/index.html')); 54 | } else { 55 | const index_path = path.join('file://', app.getAppPath(), 'dist', 'index.html'); 56 | win.loadURL(index_path); 57 | } 58 | } 59 | 60 | app.whenReady().then(async () => { 61 | await create_window(); 62 | app.on('activate', async () => { 63 | if (BrowserWindow.getAllWindows().length === 0) { 64 | await create_window(); 65 | } 66 | }); 67 | }); 68 | 69 | app.on('window-all-closed', () => { 70 | if (process.platform !== 'darwin') { 71 | app.quit(); 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /electron/preload/preload.js: -------------------------------------------------------------------------------- 1 | const { contextBridge, ipcRenderer } = require('electron'); 2 | 3 | contextBridge.exposeInMainWorld('config_api', { 4 | get_config: (file_name) => ipcRenderer.invoke('config:get-config', file_name), 5 | set_config: (file_name, config) => ipcRenderer.invoke('config:set-config', file_name, config), 6 | }); -------------------------------------------------------------------------------- /electron/third_party/webgpu_inspector/background.js: -------------------------------------------------------------------------------- 1 | (()=>{const e=new Map;chrome.runtime.onConnect.addListener((n=>{n.onMessage.addListener(((n,s)=>{const t=void 0!==n.tabId?n.tabId:s.sender.tab.id;e.has(t)||e.set(t,new Map);const a=e.get(t);a.has(s.name)||a.set(s.name,[]);const p=a.get(s.name);p.includes(s)||(p.push(s),s.onDisconnect.addListener((()=>{p.includes(s)&&p.splice(p.indexOf(s),1),0===p.length&&a.delete(s.name),0===a.size&&e.delete(t)})));const o=(e,n)=>{e.forEach((e=>{e.postMessage(n)}))};'webgpu-inspector-panel'===s.name&&a.has('webgpu-inspector-page')&&o(a.get('webgpu-inspector-page'),n),'webgpu-inspector-page'===s.name&&a.has('webgpu-inspector-panel')&&o(a.get('webgpu-inspector-panel'),n)}))}))})(); 2 | //# sourceMappingURL=background.js.map 3 | -------------------------------------------------------------------------------- /electron/third_party/webgpu_inspector/content_script.js: -------------------------------------------------------------------------------- 1 | !function(){const e={CaptureBufferData:'webgpu_inspect_capture_buffer_data',CaptureBuffers:'webgpu_inspect_capture_buffers',DeleteObjects:'webgpu_inspect_delete_objects',ValidationError:'webgpu_inspect_validation_error',MemoryLeakWarning:'webgpu_inspect_memory_leak_warning',DeltaTime:'webgpu_inspect_delta_time',CaptureFrameResults:'webgpu_inspect_capture_frame_results',CaptureFrameCommands:'webgpu_inspect_capture_frame_commands',ObjectSetLabel:'webgpu_inspect_object_set_label',AddObject:'webgpu_inspect_add_object',ResolveAsyncObject:'webgpu_inspect_resolve_async_object',DeleteObject:'webgpu_inspect_delete_object',CaptureTextureFrames:'webgpu_inspect_capture_texture_frames',CaptureTextureData:'webgpu_inspect_capture_texture_data',CaptureBufferData:'webgpu_inspect_capture_buffer_data',Recording:'webgpu_record_recording',RecordingCommand:'webgpu_record_command',RecordingDataCount:'webgpu_record_data_count',RecordingData:'webgpu_record_data'};e.values=new Set(Object.values(e));const t='webgpu_inspect_request_texture',s='webgpu_inspect_compile_shader',r='webgpu_inspect_revert_shader',n='webgpu_inspector_capture',o='webgpu_initialize_inspector',a='webgpu_initialize_recorder',i='WEBGPU_INSPECTOR_LOADED',c='WEBGPU_RECORDER_LOADED',_=new class p{constructor(e,t,s){this.name=e,this.tabId=t??0,this.listeners=[],s&&this.listeners.push(s),this._port=null,this.reset()}reset(){const e=this;this._port=chrome.runtime.connect({name:this.name}),this._port.onDisconnect.addListener((()=>{e.reset()})),this._port.onMessage.addListener((t=>{for(const s of e.listeners)s(t)}))}addListener(e){this.listeners.push(e)}postMessage(e){e.__webgpuInspector=!0,this.tabId&&(e.tabId=this.tabId);try{this._port.postMessage(e)}catch(e){this.reset()}}}('webgpu-inspector-page',0,(e=>{let _=e.action;if(!_)return;if(_===t||_===s||_===r)return void window.dispatchEvent(new CustomEvent('__WebGPUInspector',{detail:e}));if(_===a)return sessionStorage.setItem(c,`${e.frames}%${e.filename}%${e.download}`),void setTimeout((()=>{window.location.reload()}),50);let p='true';if(_===n){const t=JSON.stringify(e);if(e.frame>=0)_=o,p=t;else{sessionStorage.setItem('WEBGPU_INSPECTOR_CAPTURE_FRAME',t);const e={__webgpuInspector:!0,__webgpuInspectorPanel:!0,action:n,data:t};window.dispatchEvent(new CustomEvent('__WebGPUInspector',{detail:e}))}}_===o&&(sessionStorage.setItem(i,p),setTimeout((()=>{window.location.reload()}),50))}));function u(e,t,s){const r=document.createElement('script');if(r.id=e,r.src=t,s)for(const e in s)r.setAttribute(e,s[e]);(document.head||document.documentElement).appendChild(r)}if(window.addEventListener('pageshow',(e=>{e.persisted&&_.reset()})),window.addEventListener('__WebGPUInspector',(t=>{const s=t.detail;if('object'!=typeof s||null===s)return;const r=s.action;if(e.values.has(r))try{_.postMessage(s)}catch(e){}})),window.addEventListener('__WebGPURecorder',(t=>{const s=t.detail;if('object'!=typeof s||null===s)return;const r=s.action;if(e.values.has(r))try{_.postMessage(s)}catch(e){}})),-1===navigator.userAgent.indexOf('Chrom')&&(-1!==navigator.userAgent.indexOf('Safari')||-1!==navigator.userAgent.indexOf('Firefox'))){sessionStorage.getItem(i)&&(console.log('[WebGPU Inspector] Fallback injection'),u('__webgpu_inspector',chrome.runtime.getURL('webgpu_inspector_loader.js')));const e=sessionStorage.getItem(c);if(e){sessionStorage.removeItem(c);const t=e.split('%');u('__webgpu_recorder',chrome.runtime.getURL('webgpu_recorder_loader.js'),{filename:t[1],frames:t[0],download:t[2],removeUnusedResources:1,messageRecording:1})}}_.postMessage({action:'PageLoaded'})}(); 2 | //# sourceMappingURL=content_script.js.map 3 | -------------------------------------------------------------------------------- /electron/third_party/webgpu_inspector/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WebGPU Inspector", 3 | "short_name": "webgpu_inspector", 4 | "version": "0.6.0", 5 | "manifest_version": 3, 6 | "description": "WebGPU Inspector Debugging Tools", 7 | "author": "Brendan Duncan", 8 | "minimum_chrome_version": "116", 9 | "icons": { 10 | "19": "res/webgpu_inspector_on-19.png", 11 | "38": "res/webgpu_inspector_on-38.png" 12 | }, 13 | "background": { 14 | "service_worker": "background.js" 15 | }, 16 | "devtools_page": "webgpu_inspector_devtools.html", 17 | "content_scripts": [ 18 | { 19 | "js": ["content_script.js"], 20 | "matches": ["http://*/*", 21 | "https://*/*", 22 | "file://*/*"], 23 | "run_at" : "document_start", 24 | "all_frames" : true 25 | }, 26 | { 27 | "js": ["webgpu_inspector_loader.js"], 28 | "matches": ["http://*/*", 29 | "https://*/*", 30 | "file://*/*"], 31 | "run_at" : "document_start", 32 | "all_frames" : true, 33 | "world": "MAIN" 34 | }, 35 | { 36 | "js": ["webgpu_recorder_loader.js"], 37 | "matches": ["http://*/*", 38 | "https://*/*", 39 | "file://*/*"], 40 | "run_at" : "document_start", 41 | "all_frames" : true, 42 | "world": "MAIN" 43 | } 44 | ], 45 | "web_accessible_resources": [ 46 | { 47 | "resources": [ 48 | "webgpu_recorder_loader.js", 49 | "webgpu_recorder.js", 50 | "webgpu_inspector_loader.js" 51 | ], 52 | "matches": ["http://*/*", 53 | "https://*/*", 54 | "file://*/*"] 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /electron/third_party/webgpu_inspector/res/webgpu_inspector_on-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/electron/third_party/webgpu_inspector/res/webgpu_inspector_on-19.png -------------------------------------------------------------------------------- /electron/third_party/webgpu_inspector/res/webgpu_inspector_on-38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/electron/third_party/webgpu_inspector/res/webgpu_inspector_on-38.png -------------------------------------------------------------------------------- /electron/third_party/webgpu_inspector/webgpu_inspector_devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /electron/third_party/webgpu_inspector/webgpu_inspector_devtools.js: -------------------------------------------------------------------------------- 1 | chrome.devtools.panels.create( 2 | "WebGPU Inspector", 3 | "/res/webgpu_inspector_on-38.png", 4 | "/webgpu_inspector_panel.html" 5 | ); 6 | -------------------------------------------------------------------------------- /electron/third_party/webgpu_inspector/webgpu_inspector_panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /electron/third_party/webgpu_inspector/webgpu_inspector_worker.js: -------------------------------------------------------------------------------- 1 | var exports;(exports={}).webgpuInspectorWorker=e=>{if(!e.__webgpuInspector){if(e.__webgpuInspector=!0,e.terminate){const t=e.terminate;e.terminate=function(){const n=t.call(e,...arguments);return e.__webgpuInspector=!1,n}}e.addEventListener('message',(e=>{e.data.__webgpuInspector&&window.dispatchEvent(new CustomEvent('__WebGPUInspector',{detail:e.data}))})),window.addEventListener('__WebGPUInspector',(t=>{e.__webgpuInspector&&t.detail.__webgpuInspector&&!t.detail.__webgpuInspectorPage&&e.postMessage(t.detail)}))}},Object.defineProperty(exports,'__esModule',{value:!0}); 2 | //# sourceMappingURL=webgpu_inspector_worker.js.map 3 | -------------------------------------------------------------------------------- /engine/src/core/application_state.js: -------------------------------------------------------------------------------- 1 | const application_state = { 2 | is_running: false, 3 | is_dev_mode: false, 4 | }; 5 | 6 | export default application_state; -------------------------------------------------------------------------------- /engine/src/core/dispatcher.js: -------------------------------------------------------------------------------- 1 | class DispatcherEvent { 2 | constructor(event_name = "") { 3 | this.event_name = event_name; 4 | this.callbacks = []; 5 | } 6 | 7 | register(callback) { 8 | this.callbacks.push(callback); 9 | } 10 | 11 | unregister(callback) { 12 | const index = this.callbacks.indexOf(callback); 13 | if (index != -1) { 14 | this.callbacks.splice(index, 1); 15 | } 16 | } 17 | 18 | broadcast(...data) { 19 | const callbacks = this.callbacks.slice(0); 20 | for (let i = 0; i < callbacks.length; ++i) { 21 | callbacks[i](...data); 22 | } 23 | } 24 | } 25 | 26 | export class Dispatcher { 27 | constructor() { 28 | this.events = {}; 29 | } 30 | 31 | dispatch(event_name, ...data) { 32 | if (this.events[event_name]) { 33 | this.events[event_name].broadcast(...data); 34 | } 35 | } 36 | 37 | on(event_name, callback) { 38 | if (!(event_name in this.events)) { 39 | this.events[event_name] = new DispatcherEvent(event_name); 40 | } 41 | this.events[event_name].register(callback); 42 | } 43 | 44 | off(event_name, callback) { 45 | if (this.events[event_name]) { 46 | this.events[event_name].unregister(callback); 47 | } 48 | if (this.events[event_name].callbacks.length === 0) { 49 | delete this.events[event_name]; 50 | } 51 | } 52 | } 53 | 54 | export const global_dispatcher = new Dispatcher(); 55 | -------------------------------------------------------------------------------- /engine/src/core/ecs/fragment.js: -------------------------------------------------------------------------------- 1 | export class Fragment { } 2 | -------------------------------------------------------------------------------- /engine/src/core/ecs/fragment_registry.js: -------------------------------------------------------------------------------- 1 | // Auto-generated by fragment_preprocessor.js 2 | // Do not edit this file directly. 3 | 4 | import { LightFragment } from "./fragments/light_fragment.js"; 5 | import { StaticMeshFragment } from "./fragments/static_mesh_fragment.js"; 6 | import { TextFragment } from "./fragments/text_fragment.js"; 7 | import { TransformFragment } from "./fragments/transform_fragment.js"; 8 | import { UserInterfaceFragment } from "./fragments/user_interface_fragment.js"; 9 | import { VisibilityFragment } from "./fragments/visibility_fragment.js"; 10 | 11 | /** 12 | * A consolidated collection of all known Fragment classes in the project 13 | * that might require GPU buffer initialization. 14 | */ 15 | export const ALL_FRAGMENT_CLASSES = [ 16 | LightFragment, 17 | StaticMeshFragment, 18 | TextFragment, 19 | TransformFragment, 20 | UserInterfaceFragment, 21 | VisibilityFragment, 22 | ]; 23 | -------------------------------------------------------------------------------- /engine/src/core/ecs/fragments/static_mesh_fragment.js: -------------------------------------------------------------------------------- 1 | import { Fragment } from "../fragment.js"; 2 | import { SolarFragmentView } from "../solar/view.js"; 3 | import { RingBufferAllocator } from "../../../memory/allocator.js"; 4 | import { Name } from "../../../utility/names.js"; 5 | 6 | /** 7 | * The StaticMesh fragment class. 8 | * Use `EntityManager.get_fragment(entity, StaticMesh)` to get a fragment instance for an entity. 9 | */ 10 | export class StaticMeshFragment extends Fragment { 11 | static id = Name.from("static_mesh"); 12 | static field_key_map = new Map(); 13 | static fields = { 14 | mesh: { 15 | ctor: BigInt64Array, 16 | elements: 1, 17 | default: 0n, 18 | gpu_buffer: false, 19 | buffer_name: "mesh", 20 | is_container: false, 21 | usage: 22 | GPUBufferUsage.STORAGE | 23 | GPUBufferUsage.COPY_DST | 24 | GPUBufferUsage.COPY_SRC, 25 | cpu_readback: false, 26 | }, 27 | material_slots: { 28 | ctor: BigInt64Array, 29 | elements: 16, 30 | default: 0n, 31 | gpu_buffer: false, 32 | buffer_name: "material_slots", 33 | is_container: false, 34 | usage: 35 | GPUBufferUsage.STORAGE | 36 | GPUBufferUsage.COPY_DST | 37 | GPUBufferUsage.COPY_SRC, 38 | cpu_readback: false, 39 | }, 40 | }; 41 | static buffer_data = new Map(); // key → { buffer: FragmentGpuBuffer, stride: number } 42 | 43 | static get view_allocator() { 44 | if (!this._view_allocator) { 45 | this._view_allocator = new RingBufferAllocator( 46 | 256, 47 | new SolarFragmentView(this), 48 | ); 49 | } 50 | return this._view_allocator; 51 | } 52 | 53 | static is_valid() { 54 | return this.id && this.fields && this.view_allocator; 55 | } 56 | 57 | static get_buffer_name(field_name) { 58 | return this.field_key_map.get(field_name); 59 | } 60 | 61 | static material_slot_stride = 16; 62 | } 63 | -------------------------------------------------------------------------------- /engine/src/core/ecs/fragments/visibility_fragment.js: -------------------------------------------------------------------------------- 1 | import { Fragment } from "../fragment.js"; 2 | import { SolarFragmentView } from "../solar/view.js"; 3 | import { RingBufferAllocator } from "../../../memory/allocator.js"; 4 | import { Name } from "../../../utility/names.js"; 5 | 6 | /** 7 | * The Visibility fragment class. 8 | * Use `EntityManager.get_fragment(entity, Visibility)` to get a fragment instance for an entity. 9 | */ 10 | export class VisibilityFragment extends Fragment { 11 | static id = Name.from("visibility"); 12 | static field_key_map = new Map(); 13 | static fields = { 14 | visible: { 15 | ctor: Uint32Array, 16 | elements: 1, 17 | default: 0, 18 | gpu_buffer: true, 19 | buffer_name: "visible", 20 | is_container: false, 21 | usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, 22 | cpu_readback: false, 23 | }, 24 | }; 25 | static buffer_data = new Map(); // key → { buffer: FragmentGpuBuffer, stride: number } 26 | 27 | static get view_allocator() { 28 | if (!this._view_allocator) { 29 | this._view_allocator = new RingBufferAllocator( 30 | 256, 31 | new SolarFragmentView(this), 32 | ); 33 | } 34 | return this._view_allocator; 35 | } 36 | 37 | static is_valid() { 38 | return this.id && this.fields && this.view_allocator; 39 | } 40 | 41 | static get_buffer_name(field_name) { 42 | return this.field_key_map.get(field_name); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /engine/src/core/ecs/meta/fragment_generator_types.js: -------------------------------------------------------------------------------- 1 | export const DataType = { 2 | FLOAT32: { array: Float32Array, default: 0.0, byte_size: 4 }, 3 | UINT8: { array: Uint8Array, default: 0, byte_size: 1 }, 4 | UINT16: { array: Uint16Array, default: 0, byte_size: 2 }, 5 | UINT32: { array: Uint32Array, default: 0, byte_size: 4 }, 6 | INT8: { array: Int8Array, default: 0, byte_size: 1 }, 7 | INT16: { array: Int16Array, default: 0, byte_size: 2 }, 8 | INT32: { array: Int32Array, default: 0, byte_size: 4 }, 9 | BIGINT64: { array: BigInt64Array, default: 0n, byte_size: 8 }, 10 | }; 11 | 12 | export const BufferType = { 13 | STORAGE: 'GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST', 14 | STORAGE_SRC: 'GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC', 15 | UNIFORM: 'GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST', 16 | UNIFORM_SRC: 'GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC', 17 | VERTEX: 'GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST', 18 | VERTEX_SRC: 'GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC', 19 | CPU_READ: 'GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST', 20 | CPU_READ_SRC: 'GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC', 21 | }; 22 | -------------------------------------------------------------------------------- /engine/src/core/minimal.js: -------------------------------------------------------------------------------- 1 | import { vec4 } from "gl-matrix"; 2 | 3 | export const MAX_BUFFERED_FRAMES = 2; 4 | 5 | export const WORLD_UP = vec4.fromValues(0, 1, 0, 0); 6 | export const WORLD_FORWARD = vec4.fromValues(0, 0, 1, 0); 7 | export const WORLD_RIGHT = vec4.fromValues(1, 0, 0, 0); 8 | 9 | export const LightType = { 10 | DIRECTIONAL: 0, 11 | POINT: 1, 12 | SPOT: 2, 13 | }; 14 | 15 | export const EntityFlags = { 16 | ALIVE: 1 << 0, 17 | DIRTY: 1 << 1, 18 | IGNORE_PARENT_SCALE: 1 << 2, 19 | IGNORE_PARENT_ROTATION: 1 << 3, 20 | TRANSFORM_DIRTY: 1 << 4, 21 | NO_AABB_UPDATE: 1 << 5, 22 | AABB_DIRTY: 1 << 6, 23 | BILLBOARD: 1 << 7, 24 | }; 25 | 26 | -------------------------------------------------------------------------------- /engine/src/core/scene.js: -------------------------------------------------------------------------------- 1 | import { EntityManager } from "./ecs/entity.js"; 2 | import { SimulationLayer } from "./simulation_layer.js"; 3 | import { DevConsole } from "../tools/dev_console.js"; 4 | 5 | import { EntityPreprocessor } from "./subsystems/entity_preprocessor.js"; 6 | import { TextProcessor } from "./subsystems/text_processor.js"; 7 | import { StaticMeshProcessor } from "./subsystems/static_mesh_processor.js"; 8 | import { TransformProcessor } from "./subsystems/transform_processor.js"; 9 | import { AABBEntityAdapter } from "./subsystems/aabb_entity_adapter.js"; 10 | import { AABBTreeDebugRenderer } from "./subsystems/aabb_debug_renderer.js"; 11 | 12 | import { cursor } from "../ui/2d/immediate.js"; 13 | import { FontCache } from "../ui/text/font_cache.js"; 14 | import { ViewProcessor } from "./subsystems/view_processor.js"; 15 | import { UI3DProcessor } from "./subsystems/ui_3d_processor.js"; 16 | import { UIProcessor } from "./subsystems/ui_processor.js"; 17 | import { SharedViewBuffer } from "./shared_data.js"; 18 | import { Renderer } from "../renderer/renderer.js"; 19 | 20 | export class Scene extends SimulationLayer { 21 | name = ""; 22 | dev_cursor_enabled = true; 23 | dev_cursor_visible = false; 24 | 25 | constructor(name) { 26 | super(); 27 | this.name = name; 28 | } 29 | 30 | init() { 31 | super.init(); 32 | 33 | Renderer.get().set_scene_id(this.name); 34 | 35 | this.context.current_view = SharedViewBuffer.add_view_data(); 36 | 37 | FontCache.auto_load_fonts(); 38 | 39 | this.setup_default_subsystems(); 40 | } 41 | 42 | cleanup() { 43 | SharedViewBuffer.remove_view_data(this.context.current_view); 44 | this.context.current_view = null; 45 | 46 | this.teardown_default_subsystems(); 47 | 48 | super.cleanup(); 49 | } 50 | 51 | update(delta_time) { 52 | super.update(delta_time); 53 | this._update_dev_cursor(); 54 | } 55 | 56 | setup_default_subsystems() { 57 | const view_processor = this.add_layer(ViewProcessor); 58 | view_processor.set_scene(this); 59 | 60 | this.add_layer(UIProcessor); 61 | this.add_layer(TextProcessor); 62 | this.add_layer(StaticMeshProcessor); 63 | this.add_layer(TransformProcessor); 64 | this.add_layer(AABBEntityAdapter); 65 | if (__DEV__) { 66 | this.add_layer(AABBTreeDebugRenderer); 67 | } 68 | 69 | const ui_3d_processor = this.add_layer(UI3DProcessor); 70 | ui_3d_processor.set_scene(this); 71 | 72 | if (__DEV__) { 73 | const dev_console = this.add_layer(DevConsole); 74 | dev_console.set_scene(this); 75 | } 76 | 77 | this.add_layer(EntityPreprocessor); 78 | } 79 | 80 | teardown_default_subsystems() { 81 | if (__DEV__) { 82 | this.remove_layer(DevConsole); 83 | this.remove_layer(AABBTreeDebugRenderer); 84 | } 85 | this.remove_layer(UI3DProcessor); 86 | this.remove_layer(AABBEntityAdapter); 87 | this.remove_layer(EntityPreprocessor); 88 | this.remove_layer(UIProcessor); 89 | this.remove_layer(TextProcessor); 90 | this.remove_layer(StaticMeshProcessor); 91 | this.remove_layer(TransformProcessor); 92 | this.remove_layer(ViewProcessor); 93 | } 94 | 95 | _update_dev_cursor() { 96 | if (!this.dev_cursor_enabled || !this.dev_cursor_visible) return; 97 | 98 | cursor({ 99 | icon: "engine/sprites/cursor.png", 100 | width: 25, 101 | height: 25, 102 | background_color: "transparent", 103 | }); 104 | } 105 | 106 | set_dev_cursor_enabled(enabled) { 107 | this.dev_cursor_enabled = enabled; 108 | } 109 | 110 | show_dev_cursor() { 111 | if (!this.dev_cursor_enabled) return; 112 | this.dev_cursor_visible = true; 113 | } 114 | 115 | hide_dev_cursor() { 116 | if (!this.dev_cursor_enabled) return; 117 | this.dev_cursor_visible = false; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /engine/src/core/scene_graph.js: -------------------------------------------------------------------------------- 1 | import { EntityManager } from "./ecs/entity.js"; 2 | import { EntityID } from "./ecs/solar/types.js"; 3 | import { Buffer } from "../renderer/buffer.js"; 4 | import { Tree } from "../memory/container.js"; 5 | import { Renderer } from "../renderer/renderer.js"; 6 | 7 | export class SceneGraph { 8 | static tree = new Tree(); 9 | static scene_graph_buffer = null; 10 | static scene_graph_layer_counts = []; 11 | static scene_graph_uniforms = []; 12 | static dirty = false; 13 | 14 | static set_parent(entity, parent) { 15 | this.tree.remove(entity); 16 | this.tree.add(parent, entity); 17 | this.dirty = true; 18 | } 19 | 20 | static get_parent(entity) { 21 | const node = this.tree.find_node(entity); 22 | const parent_node = this.tree.get_parent(node); 23 | return parent_node ? parent_node.data : null; 24 | } 25 | 26 | static set_children(entity, children) { 27 | if (Array.isArray(children)) { 28 | this.tree.add_multiple(entity, children, true /* replace_children */, true /* unique */); 29 | this.dirty = true; 30 | } 31 | } 32 | 33 | static get_children(entity) { 34 | const node = this.tree.find_node(entity); 35 | return [...this.tree.get_children(node)].map((child) => child.data); 36 | } 37 | 38 | static remove(entity) { 39 | this.tree.remove(entity); 40 | this.dirty = true; 41 | } 42 | 43 | static mark_dirty() { 44 | this.dirty = true; 45 | } 46 | 47 | static flush_gpu_buffers() { 48 | if (!this.dirty) { 49 | return; 50 | } 51 | 52 | const { result, layer_counts } = this.tree.flatten( 53 | Int32Array, 54 | (out, node, size) => { 55 | const entity_idx = node.data.id; 56 | const count = node.data.instance_count; 57 | const parent_entity_idx = this.tree.get_parent(node) 58 | ? this.tree.get_parent(node).data.id 59 | : -1; 60 | for (let i = 0; i < count; i++) { 61 | out[(size + i) * 2] = entity_idx + i; 62 | out[(size + i) * 2 + 1] = parent_entity_idx; 63 | } 64 | return count; 65 | }, 66 | (node) => EntityManager.get_entity_instance_count(node.data) * 2 67 | ); 68 | this.scene_graph_layer_counts = layer_counts; 69 | 70 | if (!result) { 71 | this.dirty = false; 72 | return; 73 | } 74 | 75 | if (!this.scene_graph_buffer || this.scene_graph_buffer.config.size < result.byteLength) { 76 | this.scene_graph_buffer = Buffer.create({ 77 | name: "scene_graph_buffer", 78 | usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, 79 | raw_data: result, 80 | force: true, 81 | }); 82 | } else { 83 | this.scene_graph_buffer.write_raw(result); 84 | } 85 | 86 | let offset = 0; 87 | this.scene_graph_uniforms = layer_counts.map((count, layer) => { 88 | const uni = Buffer.create({ 89 | name: `scene_graph_uniforms_${layer}`, 90 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 91 | raw_data: new Uint32Array([count, offset, layer]), 92 | force: true, 93 | }); 94 | offset += count; 95 | return uni; 96 | }); 97 | 98 | Renderer.get().mark_bind_groups_dirty(true); 99 | 100 | this.dirty = false; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /engine/src/core/simulation_core.js: -------------------------------------------------------------------------------- 1 | import { SharedFrameInfoBuffer } from "../core/shared_data.js"; 2 | import { profile_scope } from "../utility/performance.js"; 3 | 4 | const simulation_core_update_event_name = "simulation_core_update"; 5 | 6 | export default class SimulationCore { 7 | static simulation_layers = []; 8 | 9 | static async register_simulation_layer(layer) { 10 | this.simulation_layers.push(layer); 11 | await layer.init(); 12 | } 13 | 14 | static unregister_simulation_layer(layer) { 15 | layer.cleanup(); 16 | this.simulation_layers.splice( 17 | this.simulation_layers.indexOf(layer), 18 | 1 19 | ); 20 | } 21 | 22 | static update(delta_time) { 23 | profile_scope(simulation_core_update_event_name, () => { 24 | const time = SharedFrameInfoBuffer.get_time(); 25 | SharedFrameInfoBuffer.set_time(time + delta_time); 26 | 27 | for (const layer of this.simulation_layers) { 28 | layer.pre_update(delta_time); 29 | } 30 | 31 | for (const layer of this.simulation_layers) { 32 | layer.update(delta_time); 33 | } 34 | 35 | for (const layer of this.simulation_layers) { 36 | layer.post_update(delta_time); 37 | } 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /engine/src/core/simulation_layer.js: -------------------------------------------------------------------------------- 1 | export class LayerContext { 2 | current_view = null; 3 | } 4 | 5 | export class SimulationLayer { 6 | layers = [] 7 | context = new LayerContext(); 8 | 9 | constructor() { 10 | this.name = "SimulationLayer"; 11 | } 12 | 13 | init() {} 14 | cleanup() {} 15 | 16 | pre_update(delta_time) { 17 | for (let i = 0; i < this.layers.length; i++) { 18 | this.layers[i].pre_update(delta_time); 19 | } 20 | } 21 | 22 | update(delta_time) { 23 | for (let i = 0; i < this.layers.length; i++) { 24 | this.layers[i].update(delta_time); 25 | } 26 | } 27 | 28 | post_update(delta_time) { 29 | for (let i = 0; i < this.layers.length; i++) { 30 | this.layers[i].post_update(delta_time); 31 | } 32 | } 33 | 34 | add_layer(prototype, ...args) { 35 | let found_layer = this.get_layer(prototype); 36 | if (found_layer) { 37 | return found_layer; 38 | } 39 | 40 | const layer = new prototype(...args); 41 | layer.init(); 42 | this.layers.push(layer); 43 | return layer; 44 | } 45 | 46 | remove_layer(prototype) { 47 | const layer = this.get_layer(prototype); 48 | if (layer) { 49 | this.layers.splice(this.layers.indexOf(layer), 1); 50 | } 51 | } 52 | 53 | get_layer(prototype) { 54 | return this.layers.find(layer => layer.constructor.name === prototype.name); 55 | } 56 | } -------------------------------------------------------------------------------- /engine/src/core/simulator.js: -------------------------------------------------------------------------------- 1 | import application_state from "./application_state.js"; 2 | import SimulationCore from "./simulation_core.js"; 3 | import { EntityManager } from "./ecs/entity.js"; 4 | import { Renderer } from "../renderer/renderer.js"; 5 | import { BufferSync } from "../renderer/buffer.js"; 6 | import { DeferredShadingStrategy } from "../renderer/strategies/deferred_shading.js"; 7 | import { InputProvider } from "../input/input_provider.js"; 8 | import { MetaSystem } from "../meta/meta_system.js"; 9 | import { profile_scope } from "../utility/performance.js"; 10 | import { frame_runner } from "../utility/frame_runner.js"; 11 | import { reset_ui, flush_ui } from "../ui/2d/immediate.js"; 12 | 13 | import { ALL_FRAGMENT_CLASSES } from "./ecs/fragment_registry.js"; 14 | 15 | export class Simulator { 16 | async init(gpu_canvas_name, ui_canvas_name = null) { 17 | application_state.is_running = true; 18 | 19 | // Initialize input provider 20 | InputProvider.setup(); 21 | // Initialize meta system 22 | MetaSystem.setup(); 23 | 24 | // Initialize renderer with document canvas 25 | const canvas = document.getElementById(gpu_canvas_name); 26 | const canvas_ui = ui_canvas_name ? document.getElementById(ui_canvas_name) : null; 27 | await Renderer.create(canvas, canvas_ui, DeferredShadingStrategy, { 28 | pointer_lock: true, 29 | use_precision_float: true, 30 | }); 31 | 32 | // Initialize entity manager 33 | EntityManager.setup(ALL_FRAGMENT_CLASSES); 34 | 35 | // Refresh global shader bindings 36 | Renderer.get().refresh_global_shader_bindings(); 37 | } 38 | 39 | async add_sim_layer(sim_layer) { 40 | await SimulationCore.register_simulation_layer(sim_layer); 41 | } 42 | 43 | remove_sim_layer(sim_layer) { 44 | SimulationCore.unregister_simulation_layer(sim_layer); 45 | } 46 | 47 | async _simulate(delta_time) { 48 | if (application_state.is_running) { 49 | await BufferSync.process_syncs(); 50 | 51 | profile_scope("frame_loop", async () => { 52 | const renderer = Renderer.get(); 53 | reset_ui(renderer.canvas_ui?.width ?? 0, renderer.canvas_ui?.height ?? 0); 54 | 55 | InputProvider.update(delta_time); 56 | 57 | SimulationCore.update(delta_time); 58 | 59 | renderer.render(delta_time); 60 | 61 | flush_ui(renderer.context_ui); 62 | }); 63 | } 64 | } 65 | 66 | run() { 67 | frame_runner(this._simulate, 60); 68 | } 69 | 70 | static async create(gpu_canvas_name, ui_canvas_name = null) { 71 | const instance = new Simulator(); 72 | await instance.init(gpu_canvas_name, ui_canvas_name); 73 | return instance; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /engine/src/core/subsystems/entity_preprocessor.js: -------------------------------------------------------------------------------- 1 | import { Renderer } from "../../renderer/renderer.js"; 2 | import { Chunk } from "../ecs/solar/chunk.js"; 3 | import { SimulationLayer } from "../simulation_layer.js"; 4 | import { EntityManager } from "../ecs/entity.js"; 5 | import { profile_scope } from "../../utility/performance.js"; 6 | import { TypedQueue } from "../../memory/container.js"; 7 | 8 | const entity_preprocessor_pre_update_key = "entity_preprocessor_pre_update"; 9 | const entity_preprocessor_post_update_key = "entity_preprocessor_post_update"; 10 | const copy_gpu_to_cpu_buffers_key = "copy_gpu_to_cpu_buffers"; 11 | 12 | export class EntityPreprocessor extends SimulationLayer { 13 | _dirty_flag_retain_frames = 0; 14 | _deferred_dirty_clear_chunks = new TypedQueue(1024, 0, Uint32Array); 15 | 16 | init() { 17 | this._pre_update_internal = this._pre_update_internal.bind(this); 18 | this._post_update_internal = this._post_update_internal.bind(this); 19 | this._on_post_render_commands = this._on_post_render_commands.bind(this); 20 | Renderer.get().on_post_render(this.on_post_render.bind(this)); 21 | } 22 | 23 | pre_update(delta_time) { 24 | super.pre_update(delta_time); 25 | profile_scope(entity_preprocessor_pre_update_key, this._pre_update_internal); 26 | } 27 | 28 | _pre_update_internal() { 29 | EntityManager.process_pending_deletes(); 30 | } 31 | 32 | post_update(delta_time) { 33 | super.post_update(delta_time); 34 | profile_scope(entity_preprocessor_post_update_key, this._post_update_internal); 35 | } 36 | 37 | _post_update_internal() { 38 | EntityManager.flush_gpu_buffers(); 39 | Renderer.get().enqueue_post_commands( 40 | copy_gpu_to_cpu_buffers_key, 41 | this._on_post_render_commands 42 | ); 43 | } 44 | 45 | _on_post_render_commands(graph, frame_data, encoder) { 46 | EntityManager.copy_gpu_to_cpu_buffers(encoder); 47 | } 48 | 49 | on_post_render() { 50 | EntityManager.sync_all_buffers(); 51 | this.clear_all_dirty(); 52 | } 53 | 54 | clear_all_dirty() { 55 | while (this._deferred_dirty_clear_chunks.length > 0) { 56 | const dirty_chunk_index = this._deferred_dirty_clear_chunks.pop(); 57 | const dirty_chunk = Chunk.get(dirty_chunk_index); 58 | dirty_chunk.clear_entity_dirty_flags(); 59 | } 60 | for (const dirty_chunk of Chunk.dirty) { 61 | dirty_chunk.clear_dirty(); 62 | this._deferred_dirty_clear_chunks.push(dirty_chunk.chunk_index); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /engine/src/core/subsystems/static_mesh_processor.js: -------------------------------------------------------------------------------- 1 | import { EntityFlags } from "../minimal.js"; 2 | import { DEFAULT_CHUNK_CAPACITY } from "../ecs/solar/types.js"; 3 | import { SimulationLayer } from "../simulation_layer.js"; 4 | import { EntityManager } from "../ecs/entity.js"; 5 | import { StaticMeshFragment } from "../ecs/fragments/static_mesh_fragment.js"; 6 | import { VisibilityFragment } from "../ecs/fragments/visibility_fragment.js"; 7 | import { MeshTaskQueue } from "../../renderer/mesh_task_queue.js"; 8 | import { profile_scope } from "../../utility/performance.js"; 9 | 10 | export class StaticMeshProcessor extends SimulationLayer { 11 | entity_query = null; 12 | 13 | constructor() { 14 | super(); 15 | } 16 | 17 | init() { 18 | this.entity_query = EntityManager.create_query([StaticMeshFragment, VisibilityFragment]); 19 | this._update_internal = this._update_internal.bind(this); 20 | this._update_internal_iter_chunk = this._update_internal_iter_chunk.bind(this); 21 | EntityManager.on_delete(this._on_delete.bind(this)); 22 | } 23 | 24 | update(delta_time) { 25 | profile_scope("static_mesh_processor_update", this._update_internal); 26 | } 27 | 28 | _update_internal_iter_chunk(chunk, flags, counts, archetype) { 29 | const dirty_flag = EntityFlags.DIRTY; 30 | const is_alive_flag = EntityFlags.ALIVE; 31 | const static_meshes = chunk.get_fragment_view(StaticMeshFragment); 32 | const visibilities = chunk.get_fragment_view(VisibilityFragment); 33 | const material_slot_stride = StaticMeshFragment.material_slot_stride; 34 | 35 | let should_dirty_chunk = false; 36 | let slot = 0; 37 | while (slot < DEFAULT_CHUNK_CAPACITY) { 38 | const entity_flags = flags[slot]; 39 | 40 | if ((flags[slot] & dirty_flag) === 0) { 41 | slot += counts[slot] || 1; 42 | continue; 43 | } 44 | 45 | if ((entity_flags & is_alive_flag) === 0) { 46 | slot += counts[slot] || 1; 47 | continue; 48 | } 49 | 50 | const mesh_id = Number(static_meshes.mesh[slot]); 51 | const material_id = Number(static_meshes.material_slots[slot * material_slot_stride]); 52 | 53 | const entity = EntityManager.get_entity_for(chunk, slot); 54 | 55 | if (mesh_id && material_id && entity.instance_count && visibilities.visible[slot]) { 56 | MeshTaskQueue.new_task(mesh_id, entity, material_id); 57 | } 58 | 59 | slot += counts[slot] || 1; 60 | 61 | should_dirty_chunk = true; 62 | } 63 | 64 | if (should_dirty_chunk) { 65 | chunk.mark_dirty(); 66 | } 67 | } 68 | 69 | _update_internal() { 70 | this.entity_query.for_each_chunk(this._update_internal_iter_chunk); 71 | } 72 | 73 | _on_delete(entity) { 74 | MeshTaskQueue.remove(entity); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /engine/src/core/subsystems/ui_3d_processor.js: -------------------------------------------------------------------------------- 1 | import { SimulationLayer } from "../simulation_layer.js"; 2 | import { EntityManager } from "../ecs/entity.js"; 3 | import { UserInterfaceFragment } from "../ecs/fragments/user_interface_fragment.js"; 4 | import { Element3D } from "../../ui/3d/element.js"; 5 | import { InputProvider } from "../../input/input_provider.js"; 6 | import { InputKey } from "../../input/input_types.js"; 7 | import { profile_scope } from "../../utility/performance.js"; 8 | 9 | export class UI3DProcessor extends SimulationLayer { 10 | entity_query = null; 11 | scene = null; 12 | 13 | init() { 14 | this.entity_query = EntityManager.create_query([UserInterfaceFragment]); 15 | this._update_internal = this._update_internal.bind(this); 16 | } 17 | 18 | update(delta_time) { 19 | profile_scope("UI3DProcessor.update", this._update_internal); 20 | } 21 | 22 | _update_internal() { 23 | this.entity_query.for_each((chunk, slot, instance_count, archetype) => { 24 | const entity_flags = chunk.flags_meta[slot]; 25 | if ((entity_flags & EntityFlags.ALIVE) === 0) { 26 | return; 27 | } 28 | 29 | const entity = EntityManager.get_entity_for(chunk, slot); 30 | 31 | for (let i = 0; i < instance_count; ++i) { 32 | const user_interfaces = chunk.get_fragment_view(UserInterfaceFragment); 33 | const index = slot + i; 34 | 35 | if (user_interfaces.allows_cursor_events[index]) { 36 | user_interfaces.was_cursor_inside[index] = user_interfaces.is_cursor_inside[index]; 37 | user_interfaces.is_cursor_inside[index] = entity === this.scene.get_cursor_pixel_entity(); 38 | 39 | user_interfaces.was_clicked[index] = user_interfaces.is_clicked[index]; 40 | user_interfaces.is_clicked[index] = 41 | user_interfaces.is_cursor_inside[index] && 42 | InputProvider.get_action(InputKey.B_mouse_left); 43 | 44 | user_interfaces.was_pressed[index] = user_interfaces.is_pressed[index]; 45 | user_interfaces.is_pressed[index] = 46 | user_interfaces.is_cursor_inside[index] && 47 | InputProvider.get_state(InputKey.B_mouse_left); 48 | 49 | if ( 50 | !user_interfaces.was_cursor_inside[index] && 51 | user_interfaces.is_cursor_inside[index] 52 | ) { 53 | Element3D.trigger(entity, "hover"); 54 | } else if ( 55 | user_interfaces.was_cursor_inside[index] && 56 | !user_interfaces.is_cursor_inside[index] 57 | ) { 58 | Element3D.trigger(entity, "leave"); 59 | } 60 | 61 | if (user_interfaces.is_clicked[index]) { 62 | Element3D.trigger(entity, "selected"); 63 | if (user_interfaces.consume_events[index]) { 64 | InputProvider.consume_action(InputKey.B_mouse_left); 65 | } 66 | } 67 | if (user_interfaces.is_pressed[index]) { 68 | Element3D.trigger(entity, "pressed"); 69 | if (user_interfaces.consume_events[index]) { 70 | InputProvider.consume_action(InputKey.B_mouse_left); 71 | } 72 | } 73 | 74 | chunk.mark_dirty(); 75 | } 76 | } 77 | }); 78 | } 79 | 80 | set_scene(scene) { 81 | this.scene = scene; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /engine/src/core/subsystems/ui_processor.js: -------------------------------------------------------------------------------- 1 | import { Renderer } from "../../renderer/renderer.js"; 2 | import { InputProvider } from "../../input/input_provider.js"; 3 | import { 4 | InputRange, 5 | InputKey, 6 | } from "../../input/input_types.js"; 7 | import { SimulationLayer } from "../simulation_layer.js"; 8 | import { UIContext, ImmediateUIUpdater } from "../../ui/2d/immediate.js"; 9 | import { screen_pos_to_world_pos } from "../../utility/camera.js"; 10 | import { SharedViewBuffer, SharedFrameInfoBuffer } from "../shared_data.js"; 11 | import { profile_scope } from "../../utility/performance.js"; 12 | 13 | const ui_processor_pre_update_scope_name = "UIProcessor.pre_update"; 14 | 15 | export class UIProcessor extends SimulationLayer { 16 | context = null; 17 | current_delta_time = 0; 18 | processed_keys = new Set(); 19 | 20 | init() { 21 | super.init(); 22 | 23 | const renderer = Renderer.get(); 24 | this.context = renderer.context_ui; 25 | 26 | this._pre_update_internal = this._pre_update_internal.bind(this); 27 | } 28 | 29 | pre_update(delta_time) { 30 | super.pre_update(delta_time); 31 | this.current_delta_time = delta_time; 32 | profile_scope(ui_processor_pre_update_scope_name, this._pre_update_internal); 33 | } 34 | 35 | _pre_update_internal() { 36 | const renderer = Renderer.get(); 37 | 38 | if (!UIContext.input_state.pressed) { 39 | UIContext.drag_state.active = false; 40 | UIContext.drag_state.started = false; 41 | UIContext.drag_state.widget_id = null; 42 | UIContext.drag_state.timer = 0; 43 | } 44 | 45 | UIContext.input_state.prev_x = UIContext.input_state.x; 46 | UIContext.input_state.prev_y = UIContext.input_state.y; 47 | UIContext.input_state.x = InputProvider.get_range(InputRange.M_xabs); 48 | UIContext.input_state.y = InputProvider.get_range(InputRange.M_yabs); 49 | UIContext.input_state.clicked = InputProvider.get_action(InputKey.B_mouse_left); 50 | UIContext.input_state.pressed = InputProvider.get_state(InputKey.B_mouse_left); 51 | UIContext.input_state.wheel = InputProvider.get_range(InputRange.M_wheel) * 0.01; 52 | UIContext.delta_time = this.current_delta_time; 53 | 54 | if (UIContext.drag_state.active) { 55 | UIContext.drag_state.timer += this.current_delta_time; 56 | } 57 | 58 | this.processed_keys.clear(); 59 | 60 | const keyboard_events = InputProvider.current_dirty_states; 61 | 62 | for (let i = 0; i < keyboard_events.length; i++) { 63 | const key = keyboard_events[i].raw_input; 64 | 65 | if (!this.processed_keys.has(key)) { 66 | const has_state = InputProvider.get_state(key); 67 | const has_action = InputProvider.get_action(key); 68 | 69 | const new_key = UIContext.keyboard_events.allocate(); 70 | new_key.key = key; 71 | new_key.first = has_action; 72 | new_key.held = has_state && !has_action; 73 | new_key.consumed = false; 74 | new_key.last_change_time = keyboard_events[i].last_change_time; 75 | 76 | this.processed_keys.add(key); 77 | } 78 | } 79 | 80 | UIContext.input_state.world_position = screen_pos_to_world_pos( 81 | SharedViewBuffer.get_view_data(0), 82 | UIContext.input_state.x, 83 | UIContext.input_state.y, 84 | renderer.canvas.width, 85 | renderer.canvas.height, 86 | 1.0 87 | ); 88 | SharedFrameInfoBuffer.set_cursor_world_position(UIContext.input_state.world_position); 89 | 90 | this.context.clearRect(0, 0, renderer.canvas.width, renderer.canvas.height); 91 | 92 | ImmediateUIUpdater.update_all(this.current_delta_time); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /engine/src/core/subsystems/view_processor.js: -------------------------------------------------------------------------------- 1 | import { Renderer } from '../../renderer/renderer.js'; 2 | import { SimulationLayer } from '../simulation_layer.js'; 3 | import { SharedViewBuffer } from '../shared_data.js'; 4 | import { global_dispatcher } from '../dispatcher.js'; 5 | 6 | export class ViewProcessor extends SimulationLayer { 7 | scene = null; 8 | 9 | init() { 10 | super.init(); 11 | global_dispatcher.on("resolution_change", this.on_resolution_change.bind(this)); 12 | } 13 | 14 | cleanup() { 15 | scene = null; 16 | global_dispatcher.off("resolution_change", this.on_resolution_change.bind(this)); 17 | super.cleanup(); 18 | } 19 | 20 | update(delta_time) { 21 | super.update(delta_time); 22 | SharedViewBuffer.update_transforms(this.context.current_view); 23 | } 24 | 25 | set_scene(scene) { 26 | this.scene = scene; 27 | this.context.current_view = scene.context.current_view; 28 | SharedViewBuffer.set_view_data(this.scene.context.current_view, { 29 | aspect_ratio: Renderer.get().aspect_ratio, 30 | }); 31 | } 32 | 33 | on_resolution_change() { 34 | SharedViewBuffer.set_view_data(this.scene.context.current_view, { 35 | aspect_ratio: Renderer.get().aspect_ratio, 36 | }); 37 | } 38 | } -------------------------------------------------------------------------------- /engine/src/input/input_context.js: -------------------------------------------------------------------------------- 1 | export class InputContext { 2 | input_states = []; 3 | action_mappings = []; 4 | state_mappings = []; 5 | range_mappings = []; 6 | raw_to_state_mappings = {}; 7 | action_to_state_mappings = {}; 8 | dirty_states = new Set(); 9 | 10 | constructor(states = []) { 11 | this.set_states(states); 12 | } 13 | 14 | num_states() { 15 | return this.input_states.length; 16 | } 17 | 18 | set_states(states) { 19 | this.input_states = states; 20 | this.action_mappings = new Array(this.input_states.length).fill(false); 21 | this.state_mappings = new Array(this.input_states.length).fill(false); 22 | this.range_mappings = new Array(this.input_states.length).fill(0.0); 23 | this.raw_to_state_mappings = {}; 24 | this.action_to_state_mappings = {}; 25 | this.dirty_states = new Set(); 26 | 27 | for (let i = 0; i < this.input_states.length; ++i) { 28 | const input_state = this.input_states[i]; 29 | this.raw_to_state_mappings[input_state.raw_input] = i; 30 | this.action_to_state_mappings[input_state.mapped_name] = i; 31 | } 32 | } 33 | 34 | set_state(input_state_index, new_state) { 35 | console.assert(input_state_index < this.state_mappings.length && input_state_index >= 0, 'Invalid input state index'); 36 | if (this.state_mappings[input_state_index] !== new_state) { 37 | this.input_states[input_state_index].last_change_time = performance.now(); 38 | } 39 | this.state_mappings[input_state_index] = new_state; 40 | if (new_state) { 41 | this.dirty_states.add(input_state_index); 42 | } 43 | } 44 | 45 | set_action(input_state_index, new_action) { 46 | console.assert(input_state_index < this.action_mappings.length && input_state_index >= 0, 'Invalid input state index'); 47 | if (this.action_mappings[input_state_index] !== new_action) { 48 | this.input_states[input_state_index].last_change_time = performance.now(); 49 | } 50 | const action_fired = !this.action_mappings[input_state_index] && new_action; 51 | this.action_mappings[input_state_index] = new_action; 52 | if (action_fired) { 53 | this.dirty_states.add(input_state_index); 54 | } 55 | } 56 | 57 | set_range(input_state_index, new_range) { 58 | console.assert(input_state_index < this.range_mappings.length && input_state_index >= 0, 'Invalid input state index'); 59 | if (this.range_mappings[input_state_index] !== new_range) { 60 | this.input_states[input_state_index].last_change_time = performance.now(); 61 | } 62 | if (new_range !== 0.0) { 63 | this.range_mappings[input_state_index] = new_range; 64 | this.input_states[input_state_index].range_value = new_range; 65 | this.dirty_states.add(input_state_index); 66 | } 67 | } 68 | 69 | visit_dirty_states(visitor) { 70 | if (typeof visitor !== 'function') { 71 | return; 72 | } 73 | 74 | for (const index of this.dirty_states) { 75 | console.assert(index < this.input_states.length && index >= 0, 'Invalid dirty state index'); 76 | const state = this.input_states[index]; 77 | visitor(state); 78 | } 79 | 80 | this.dirty_states.clear(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /engine/src/meta/meta_system.js: -------------------------------------------------------------------------------- 1 | export class MetaSystem { 2 | static setup() { } 3 | } 4 | -------------------------------------------------------------------------------- /engine/src/ml/layers/binary_cross_entropy_loss.js: -------------------------------------------------------------------------------- 1 | import { MLOps } from "../ops/ops.js"; 2 | import { Name } from "../names.js"; 3 | 4 | const binary_cross_entropy_loss_name = "binary_cross_entropy_loss"; 5 | 6 | export class BinaryCrossEntropyLoss { 7 | static initialize(layer) { } 8 | 9 | static forward(layer, input_tensor, target_tensor = null) { 10 | const props = layer.properties; 11 | layer.loss = MLOps.binary_cross_entropy_loss(target_tensor, input_tensor, props.enabled_logging, Name.from(props.name || binary_cross_entropy_loss_name)); 12 | return input_tensor; 13 | } 14 | 15 | static backward(layer, grad_output_tensor, target_tensor = null) { 16 | return MLOps.binary_cross_entropy_loss_prime(target_tensor, grad_output_tensor); 17 | } 18 | } -------------------------------------------------------------------------------- /engine/src/ml/layers/cross_entropy_loss.js: -------------------------------------------------------------------------------- 1 | import { MLOps } from "../ops/ops.js"; 2 | import { Name } from "../names.js"; 3 | 4 | const cross_entropy_loss_name = "cross_entropy_loss"; 5 | 6 | export class CrossEntropyLoss { 7 | static initialize(layer) { } 8 | 9 | static forward(layer, input_tensor, target_tensor = null) { 10 | const props = layer.properties; 11 | layer.loss = MLOps.cross_entropy_loss(target_tensor, input_tensor, props.enabled_logging, Name.from(props.name || cross_entropy_loss_name)); 12 | return input_tensor; 13 | } 14 | 15 | static backward(layer, grad_output_tensor, target_tensor = null) { 16 | return MLOps.cross_entropy_loss_prime(target_tensor, grad_output_tensor); 17 | } 18 | } -------------------------------------------------------------------------------- /engine/src/ml/layers/fully_connected.js: -------------------------------------------------------------------------------- 1 | import { Tensor, TensorInitializer } from "../math/tensor.js"; 2 | 3 | export class FullyConnected { 4 | static initialize(layer) { 5 | const props = layer.properties; 6 | const input_size = props.input_size; 7 | const output_size = props.output_size; 8 | 9 | // Unified parameter array to hold both weights and bias. 10 | // It has a shape of [(inputSize + 1), outputSize], where the last row holds the bias. 11 | // Initialize weight values using He initialization. 12 | if (layer.params === null) { 13 | const initializer = props.initializer || TensorInitializer.HE; 14 | layer.params = Tensor.init_tensor([(input_size + 1), output_size], initializer, output_size); 15 | // Initialize bias values (last row) to zero (or use provided options if any). 16 | layer.params.fill(0, input_size * output_size); 17 | } 18 | 19 | // Make the params persistent so that it can be reused across multiple forward and backward passes. These are our stored weights after all. 20 | layer.params.persistent = true; 21 | } 22 | 23 | static forward(layer, input_tensor, target_tensor = null) { 24 | // Build an extended input by appending 1 to each row for the bias. 25 | // Extended input has shape [batch_size, input_size + 1]. 26 | // Cache input for the backward pass. 27 | layer.cached_input = input_tensor.extend([0, 1], 1); 28 | 29 | // Compute output = extended_input dot params. 30 | // This multiplication handles both the linear transform and bias addition. 31 | layer.cached_output = layer.cached_input.mat_mul(layer.params); 32 | 33 | return layer.cached_output; 34 | } 35 | 36 | static backward(layer, grad_output_tensor, target_tensor = null) { 37 | const props = layer.properties; 38 | 39 | // Transpose the extended input. 40 | const extended_input_t = layer.cached_input.transpose(); 41 | 42 | // Compute gradient for unified parameters: 43 | // grad_params = (extended_input)^T dot grad_output. 44 | // Result has shape [(input_size + 1), output_size]. 45 | layer.grad_params = extended_input_t.mat_mul(grad_output_tensor); 46 | // Clip the L2 norm of the gradient parameters to 1.0 to prevent exploding gradients. 47 | layer.grad_params.clip_l2_norm(1.0); 48 | 49 | // Compute gradient with respect to the input. 50 | // To do this, we use only the weight part of our unified parameter array (exclude the bias row). 51 | const weights = Tensor.zeros([props.input_size, props.output_size]); 52 | weights.copy(layer.params, 0, weights.length); 53 | 54 | // Transpose weights to shape [output_size, input_size] for multiplication. 55 | const weights_t = weights.transpose(); 56 | 57 | return grad_output_tensor.mat_mul(weights_t); 58 | } 59 | 60 | static update_parameters(layer, learning_rate, optimizer = null, weight_decay = 0) { 61 | if (optimizer === null) { 62 | // Update the weights using the gradient tensor. 63 | // SGD is used by default when the input / grad_params tensors have a single batch and there is no optimizer. 64 | // Otherwise, we reduce the gradient over the batch dimension (for mini-batching) and update the weights. 65 | const reduced_grad_params = layer.grad_params.batch_reduce_mean(); 66 | const grad_params_scaled = reduced_grad_params.scale(learning_rate); 67 | layer.params.sub_assign(grad_params_scaled); 68 | } else { 69 | optimizer.apply_gradients(layer.params, layer.grad_params, learning_rate, weight_decay); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /engine/src/ml/layers/mse_loss.js: -------------------------------------------------------------------------------- 1 | import { MLOps } from "../ops/ops.js"; 2 | import { Name } from "../names.js"; 3 | 4 | const mse_loss_name = 'mse_loss'; 5 | 6 | export class MSELoss { 7 | static initialize(layer) { } 8 | 9 | static forward(layer, input_tensor, target_tensor = null) { 10 | const props = layer.properties; 11 | layer.loss = MLOps.mse_loss(target_tensor, input_tensor, props.enabled_logging, Name.from(props.name || mse_loss_name)); 12 | return input_tensor; 13 | } 14 | 15 | static backward(layer, grad_output_tensor, target_tensor = null) { 16 | return MLOps.mse_loss_prime(target_tensor, grad_output_tensor); 17 | } 18 | } -------------------------------------------------------------------------------- /engine/src/ml/layers/relu.js: -------------------------------------------------------------------------------- 1 | export class ReLU { 2 | static initialize(layer) { } 3 | 4 | static forward(layer, input_tensor, target_tensor = null) { 5 | layer.cached_input = input_tensor; 6 | layer.cached_output = input_tensor.relu(); 7 | return layer.cached_output; 8 | } 9 | 10 | static backward(layer, grad_output_tensor, target_tensor = null) { 11 | return layer.cached_output.relu_backward(grad_output_tensor); 12 | } 13 | } -------------------------------------------------------------------------------- /engine/src/ml/layers/sigmoid.js: -------------------------------------------------------------------------------- 1 | export class Sigmoid { 2 | static initialize(layer) { } 3 | 4 | static forward(layer, input_tensor, target_tensor = null) { 5 | layer.cached_input = input_tensor; 6 | layer.cached_output = input_tensor.sigmoid(); 7 | return layer.cached_output; 8 | } 9 | 10 | static backward(layer, grad_output_tensor, target_tensor = null) { 11 | return layer.cached_output.sigmoid_backward(grad_output_tensor); 12 | } 13 | } -------------------------------------------------------------------------------- /engine/src/ml/layers/softmax.js: -------------------------------------------------------------------------------- 1 | export class Softmax { 2 | static initialize(layer) { } 3 | 4 | static forward(layer, input_tensor, target_tensor = null) { 5 | layer.cached_input = input_tensor; 6 | layer.cached_output = input_tensor.softmax(); 7 | return layer.cached_output; 8 | } 9 | 10 | static backward(layer, grad_output_tensor, target_tensor = null) { 11 | return layer.cached_output.softmax_backward(grad_output_tensor); 12 | } 13 | } -------------------------------------------------------------------------------- /engine/src/ml/layers/tanh.js: -------------------------------------------------------------------------------- 1 | export class Tanh { 2 | static initialize(layer) { } 3 | 4 | static forward(layer, input_tensor, target_tensor = null) { 5 | layer.cached_input = input_tensor; 6 | layer.cached_output = input_tensor.tanh(); 7 | return layer.cached_output; 8 | } 9 | 10 | static backward(layer, grad_output_tensor, target_tensor = null) { 11 | return layer.cached_output.tanh_backward(grad_output_tensor); 12 | } 13 | } -------------------------------------------------------------------------------- /engine/src/ml/logger.js: -------------------------------------------------------------------------------- 1 | import { log, warn, error } from "../utility/logging.js"; 2 | 3 | const info_name = 'info'; 4 | const warn_name = 'warn'; 5 | const error_name = 'error'; 6 | 7 | /** 8 | * Simple logger for ML operations that supports both console logging 9 | 10 | * and buffered logging for external processing 11 | */ 12 | class Logger { 13 | _buffer = []; 14 | _console_enabled = true; 15 | _buffer_enabled = false; 16 | 17 | /** 18 | * Enable or disable console logging 19 | * @param {boolean} enabled 20 | */ 21 | set_console_logging(enabled) { 22 | this._console_enabled = enabled; 23 | } 24 | 25 | /** 26 | * Enable or disable buffered logging 27 | * @param {boolean} enabled 28 | */ 29 | set_buffer_logging(enabled) { 30 | this._buffer_enabled = enabled; 31 | } 32 | 33 | /** 34 | * Log a message 35 | * @param {string} message 36 | * @param {string} [level='info'] - Log level (info, warn, error) 37 | */ 38 | log(message, level = info_name) { 39 | const formatted_message = `[ML] [${level.toUpperCase()}] ${message}`; 40 | if (this._console_enabled) { 41 | switch (level) { 42 | case warn_name: 43 | warn(formatted_message); 44 | break; 45 | case error_name: 46 | error(formatted_message); 47 | break; 48 | default: 49 | log(formatted_message); 50 | } 51 | } 52 | if (this._buffer_enabled) { 53 | this._buffer.push({ message: formatted_message, timestamp: Date.now() }); 54 | } 55 | } 56 | 57 | /** 58 | * Get and clear the log buffer 59 | * @returns {Array<{message: string, timestamp: number}>} 60 | */ 61 | flush() { 62 | const logs = [...this._buffer]; 63 | this._buffer = []; 64 | return logs; 65 | } 66 | 67 | /** 68 | * Get the current log buffer without clearing it 69 | * @returns {Array<{message: string, timestamp: number}>} 70 | */ 71 | peek() { 72 | return this._buffer; 73 | } 74 | 75 | /** 76 | * Pop a log from the buffer 77 | * @returns {Array<{message: string, timestamp: number}>} 78 | */ 79 | pop(count = 1) { 80 | return this._buffer.splice(0, count); 81 | } 82 | 83 | /** 84 | * Clear the log buffer 85 | */ 86 | clear() { 87 | this._buffer = []; 88 | } 89 | } 90 | 91 | // Export a singleton instance for convenience 92 | export const logger = new Logger(); -------------------------------------------------------------------------------- /engine/src/ml/ml_types.js: -------------------------------------------------------------------------------- 1 | export const ModelType = Object.freeze({ 2 | NEURAL: 0, 3 | }); 4 | 5 | export const OptimizerType = Object.freeze({ 6 | ADAM: 0, 7 | SGD: 1, 8 | }); 9 | 10 | export const LayerType = Object.freeze({ 11 | INPUT: 0, 12 | FULLY_CONNECTED: 1, 13 | CONVOLUTIONAL: 2, 14 | POOLING: 3, 15 | DROPOUT: 4, 16 | MSE: 5, 17 | CROSS_ENTROPY: 6, 18 | BINARY_CROSS_ENTROPY: 7, 19 | RELU: 8, 20 | TANH: 9, 21 | SIGMOID: 10, 22 | SOFTMAX: 11, 23 | }); 24 | 25 | export const InputType = Object.freeze({ 26 | IMAGE: 0, 27 | NUMERIC: 1, 28 | TEXT: 2, 29 | }); 30 | 31 | -------------------------------------------------------------------------------- /engine/src/ml/names.js: -------------------------------------------------------------------------------- 1 | export class Name { 2 | static string_to_hash = new Map(); 3 | static hash_to_string = new Map(); 4 | 5 | constructor(str) { 6 | if (Name.string_to_hash.has(str)) { 7 | this.hash = Name.string_to_hash.get(str); 8 | } else { 9 | this.hash = Name.fnv1a_hash(str); 10 | Name.string_to_hash.set(str, this.hash); 11 | Name.hash_to_string.set(this.hash, str); 12 | } 13 | } 14 | 15 | static fnv1a_hash(str) { 16 | let hash = 0x811c9dc5; 17 | for (let i = 0; i < str.length; i++) { 18 | hash ^= str.charCodeAt(i); 19 | hash = (hash * 0x01000193) >>> 0; // Force 32-bit unsigned integer 20 | } 21 | return hash; 22 | } 23 | 24 | static string(hash) { 25 | return Name.hash_to_string.get(hash); 26 | } 27 | 28 | static hash(str) { 29 | return Name.string_to_hash.get(str); 30 | } 31 | 32 | static from(str) { 33 | const name = new Name(str); 34 | return Name.hash(str); 35 | } 36 | } -------------------------------------------------------------------------------- /engine/src/ml/neural_architecture.js: -------------------------------------------------------------------------------- 1 | import { Layer } from "./layer.js"; 2 | 3 | export class NeuralArchitectureHelpers { 4 | /** 5 | * Runs inference on the neural network based on the input tensor and the root layer ID. 6 | * 7 | * @param {number} root_id - The ID of the root layer. 8 | * @param {Object} input_tensor - The input tensor. 9 | * @returns {Object} The output tensor. 10 | 11 | */ 12 | static predict(root_id, input_tensor) { 13 | return Layer.forward(root_id, input_tensor); 14 | } 15 | 16 | /** 17 | * Trains on a single sample of data from the root layer. 18 | * 19 | * @param {number} root_id - The ID of the root layer. 20 | * @param {Object} input_tensor - Training inputs: { data: Float32Array, shape: [...], batch_size: number } 21 | * @param {Object} target_tensor - Expected outputs: { data: Float32Array, shape: [...], batch_size: number } 22 | * @param {Object} output_layer - The output layer to cache the output of. 23 | * @returns {Object} The output tensor. 24 | */ 25 | static train(root_id, input_tensor, target_tensor, output_layer = null) { 26 | // Forward pass. 27 | let output = Layer.forward(root_id, input_tensor, target_tensor); 28 | // Backward pass: propagate gradients from the last layer back. 29 | Layer.backward(root_id, output, target_tensor); 30 | // Return the predicted output tensor for this training step. 31 | return output; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /engine/src/ml/ops/hop_api_adapter.js: -------------------------------------------------------------------------------- 1 | import { LayerType, OptimizerType, InputType } from "../ml_types.js"; 2 | import { Layer, TrainingContext } from "../layer.js"; 3 | import { Input } from "../layers/input.js"; 4 | import { Tensor } from "../math/tensor.js"; 5 | 6 | import { Adam } from "../optimizers/adam.js"; 7 | 8 | export class HopAPIAdapter { 9 | static set_subnet_context(subnet_id, options = {}) { 10 | const context = new TrainingContext(options); 11 | Layer.set_subnet_context(subnet_id, context); 12 | return context; 13 | } 14 | 15 | static set_subnet_context_property(subnet_id, prop_name, value) { 16 | return Layer.set_layer_context_property(subnet_id, prop_name, value); 17 | } 18 | 19 | static add_input(capacity, batch_size, parent = null) { 20 | return Layer.create(LayerType.INPUT, { capacity, batch_size }, parent); 21 | } 22 | 23 | static add_layer(type, input_size, output_size, parent = null, options = {}, params = null) { 24 | let layer = null; 25 | 26 | if (type === LayerType.FULLY_CONNECTED) { 27 | layer = Layer.create(type, { input_size, output_size, ...options }, parent, params); 28 | } 29 | 30 | return layer; 31 | } 32 | 33 | static add_activation(type, parent = null) { 34 | return Layer.create(type, {}, parent); 35 | } 36 | 37 | static add_loss(type, enabled_logging = false, name = null, parent = null) { 38 | return Layer.create(type, { enabled_logging, name }, parent); 39 | } 40 | 41 | static push_samples(source_layer_id, data, shape, batch_size, input_type = InputType.NUMERIC) { 42 | const input_layer = Layer.get(source_layer_id); 43 | if (input_layer.type !== LayerType.INPUT) { 44 | throw new Error("Source layer is not an input layer"); 45 | } 46 | 47 | const tensor = Tensor.create(data, shape, batch_size, data.constructor); 48 | Input.set_input_type(input_layer, input_type); 49 | Input.add_sample_batch(input_layer, tensor, tensor); 50 | 51 | return tensor; 52 | } 53 | 54 | static set_optimizer(type, root = null, beta1 = 0.9, beta2 = 0.999, epsilon = 1e-8) { 55 | let optimizer = null; 56 | 57 | if (type === OptimizerType.ADAM) { 58 | optimizer = new Adam(beta1, beta2, epsilon); 59 | } 60 | 61 | if (root) { 62 | Layer.set_layer_context_property(root, 'optimizer', optimizer); 63 | } 64 | 65 | return optimizer; 66 | } 67 | 68 | static clear_model(root) { 69 | Layer.destroy(root); 70 | } 71 | 72 | /** 73 | * Connects one layer to another layer 74 | * 75 | * @param {number} source_layer_id - ID of the layer to be connected 76 | * @param {number} target_layer_id - ID of the layer to connect to as a parent 77 | * @returns {boolean} True if the operation was successful 78 | */ 79 | static connect_layer(source_layer_id, target_layer_id) { 80 | return Layer.connect(source_layer_id, target_layer_id); 81 | } 82 | 83 | /** 84 | * Disconnects a layer from its parent 85 | * 86 | * @param {number} parent_id - ID of the parent layer 87 | * @param {number} layer_id - ID of the layer to disconnect 88 | * @returns {boolean} True if the operation was successful 89 | */ 90 | static disconnect_layer(parent_id, layer_id) { 91 | return Layer.disconnect(parent_id, layer_id); 92 | } 93 | 94 | /** 95 | * Disconnects a layer from all its parents 96 | * 97 | * @param {number} layer_id - ID of the layer to disconnect 98 | * @returns {boolean} True if the operation was successful 99 | */ 100 | static disconnect_layer_from_all(layer_id) { 101 | return Layer.disconnect_all(layer_id); 102 | } 103 | 104 | /** 105 | * Reorders a layer within its parent's children 106 | * 107 | * @param {number} layer_id - ID of the layer to reorder 108 | * @param {number} target_index - New index for the layer 109 | * @returns {boolean} True if the operation was successful 110 | */ 111 | static reorder_layer(layer_id, target_index) { 112 | return Layer.reorder_in_parent(layer_id, target_index); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /engine/src/ml/ops/op_types.js: -------------------------------------------------------------------------------- 1 | // An enum of all supported low-level operations. 2 | export const MLOpType = Object.freeze({ 3 | NONE: 0, 4 | CREATE_ZERO_TENSOR: 1, 5 | INIT_RANDOM: 2, 6 | INIT_HE: 3, 7 | INIT_GLOROT: 4, 8 | MAT_MUL: 5, 9 | TRANSPOSE: 6, 10 | FILL: 7, 11 | EXTEND: 8, 12 | RESHAPE: 9, 13 | COPY: 10, 14 | CLONE: 11, 15 | ADD: 12, 16 | SUB: 13, 17 | SUB_ASSIGN: 14, 18 | DIV: 15, 19 | SCALE: 16, 20 | FUSED_MUL_ADD: 17, 21 | RELU: 18, 22 | RELU_BACKWARD: 19, 23 | TANH: 20, 24 | TANH_BACKWARD: 21, 25 | SIGMOID: 22, 26 | SIGMOID_BACKWARD: 23, 27 | MSE_LOSS: 24, 28 | MSE_LOSS_PRIME: 25, 29 | SOFTMAX: 26, 30 | SOFTMAX_BACKWARD: 27, 31 | CROSS_ENTROPY_LOSS: 28, 32 | CROSS_ENTROPY_LOSS_PRIME: 29, 33 | BINARY_CROSS_ENTROPY_LOSS: 30, 34 | BINARY_CROSS_ENTROPY_LOSS_PRIME: 31, 35 | CLIP_L2_NORM: 32, 36 | BATCH_REDUCE_MEAN: 33, 37 | ADAM_MOMENT_UPDATE: 34, 38 | WRITE_TENSOR: 35, 39 | READ_TENSOR: 36, 40 | }); 41 | 42 | // An enum of all supported high-level operations. 43 | export const MLHopType = Object.freeze({ 44 | NONE: 0, 45 | SET_SUBNET_CONTEXT: 1, 46 | SET_SUBNET_CONTEXT_PROPERTY: 2, 47 | ADD_INPUT: 3, 48 | ADD_LAYER: 4, 49 | ADD_ACTIVATION: 5, 50 | ADD_LOSS: 6, 51 | PUSH_SAMPLES: 7, 52 | SET_OPTIMIZER: 8, 53 | RESET_MODEL: 9, 54 | CONNECT_LAYER: 10, 55 | DISCONNECT_LAYER: 11, 56 | DISCONNECT_LAYER_FROM_ALL: 12, 57 | REORDER_LAYER: 13, 58 | MERGE_MODELS: 14, 59 | }); 60 | 61 | // A class that represents a low-level operation and all associated parameters. 62 | export class MLOp { 63 | type = MLOpType.NONE; 64 | param_start = 0; 65 | param_count = 0; 66 | result = -1; 67 | } 68 | 69 | // A class that represents the parameter list of a low-level operation. 70 | export class MLOpParams { 71 | data = null; 72 | #size = 0; 73 | #capacity = 0; 74 | 75 | constructor(size) { 76 | this.data = new Float64Array(size); 77 | this.#capacity = size; 78 | } 79 | 80 | reset() { 81 | this.#size = 0; 82 | } 83 | 84 | resize_if_necessary(new_size) { 85 | if (new_size > this.#capacity) { 86 | this.#capacity = new_size * 2; 87 | const new_data = new Float64Array(this.#capacity); 88 | new_data.set(this.data); 89 | this.data = new_data; 90 | } 91 | } 92 | 93 | add(data, size = null) { 94 | let true_size; 95 | 96 | if (Array.isArray(data) || ArrayBuffer.isView(data)) { 97 | true_size = size ?? data.length; 98 | this.resize_if_necessary(this.#size + true_size); 99 | this.data.set(data, this.#size); 100 | this.#size += true_size; 101 | } else { 102 | true_size = 1; 103 | this.resize_if_necessary(this.#size + true_size); 104 | this.data[this.#size] = data; 105 | this.#size += true_size; 106 | } 107 | } 108 | 109 | get(offset, size) { 110 | return this.data.subarray(offset, offset + size); 111 | } 112 | 113 | get_element(index) { 114 | return this.data[index]; 115 | } 116 | 117 | set_element(index, value) { 118 | this.data[index] = value; 119 | } 120 | 121 | append(other) { 122 | this.resize_if_necessary(this.#size + other.#size); 123 | this.data.set(other.data, this.#size); 124 | this.#size += other.#size; 125 | } 126 | 127 | get length() { 128 | return this.#size; 129 | } 130 | } 131 | 132 | // A class that represents a high-level operation, usually encapsulating an API object. 133 | export class MLHop { 134 | type = MLHopType.NONE; 135 | result = null; 136 | } -------------------------------------------------------------------------------- /engine/src/ml/optimizer.js: -------------------------------------------------------------------------------- 1 | export class Optimizer { 2 | apply_gradients(tensor, grad_tensor, learning_rate = 0.001) { 3 | throw new Error("apply_gradients() not implemented."); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /engine/src/ml/optimizers/adam.js: -------------------------------------------------------------------------------- 1 | import { Tensor } from "../math/tensor.js"; 2 | import { Optimizer } from "../optimizer.js"; 3 | 4 | export class Adam extends Optimizer { 5 | constructor(beta1 = 0.9, beta2 = 0.999, epsilon = 1e-8) { 6 | super(); 7 | this.beta1 = beta1; 8 | this.beta2 = beta2; 9 | this.epsilon = epsilon; 10 | this.t = 0; // Time step for bias correction. 11 | // Maps to store first moment (m) and second moment (v) for each variable (by its tensor id) 12 | this.m = new Map(); 13 | this.v = new Map(); 14 | } 15 | 16 | // Ensure the variable is registered so that its m and v tensors are allocated. 17 | #register_variable(variable) { 18 | if (!this.m.has(variable.id)) { 19 | // Create m and v tensors with the same shape (and batch size) as the variable. 20 | const m = Tensor.zeros(variable.shape, variable.batch_size); 21 | const v = Tensor.zeros(variable.shape, variable.batch_size); 22 | m.persistent = true; 23 | v.persistent = true; 24 | this.m.set(variable.id, m); 25 | this.v.set(variable.id, v); 26 | } 27 | } 28 | 29 | // Applies an update to a variable given its gradient. 30 | // This method uses in-place updates on the variable tensor. 31 | apply_gradients(variable, grad, learning_rate = 0.001, weight_decay = 0) { 32 | // Register variable if needed. 33 | this.#register_variable(variable); 34 | 35 | const m_tensor = this.m.get(variable.id); 36 | const v_tensor = this.v.get(variable.id); 37 | 38 | this.t += 1; // Increase timestep. 39 | 40 | // First perform the Adam update with just the gradient 41 | variable.adam_moment_update( 42 | m_tensor, 43 | v_tensor, 44 | grad, 45 | this.beta1, 46 | this.beta2, 47 | this.t, 48 | this.epsilon, 49 | learning_rate 50 | ); 51 | 52 | // Then apply weight decay separately after the Adam update 53 | if (weight_decay > 0) { 54 | const decay = variable.scale(weight_decay * learning_rate); 55 | variable.sub_assign(decay); // In-place subtraction 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /engine/src/renderer/command_queue.js: -------------------------------------------------------------------------------- 1 | import { Renderer } from "./renderer.js"; 2 | 3 | export class CommandQueue { 4 | static create_encoder(name) { 5 | const renderer = Renderer.get(); 6 | return renderer.device.createCommandEncoder({ label: name }); 7 | } 8 | 9 | static submit(encoder, post_render_cb = null) { 10 | const renderer = Renderer.get(); 11 | const command_buffer = encoder.finish(); 12 | renderer.device.queue.submit([command_buffer]); 13 | renderer.device.queue.onSubmittedWorkDone().then(async () => { 14 | renderer.execution_queue.update(); 15 | if (post_render_cb) { 16 | await post_render_cb(); 17 | } 18 | }); 19 | } 20 | } -------------------------------------------------------------------------------- /engine/src/renderer/compute_raster_task_queue.js: -------------------------------------------------------------------------------- 1 | import { Buffer } from "./buffer.js"; 2 | import { Texture } from "./texture.js"; 3 | import { RandomAccessAllocator } from "../memory/allocator.js"; 4 | import { profile_scope } from "../utility/performance.js"; 5 | import { RenderPassFlags } from "./renderer_types.js"; 6 | import { Renderer } from "./renderer.js"; 7 | 8 | const compile_rg_pass_scope_name = "ComputeRasterTaskQueue.compile_rg_passes"; 9 | const workgroup_size = 256; 10 | 11 | export const ComputeRasterPrimitiveType = { 12 | Point: "point", 13 | Line: "line", 14 | Triangle: "triangle", 15 | Quad: "quad", 16 | }; 17 | 18 | const ComputeRasterPrimitiveStride = { 19 | [ComputeRasterPrimitiveType.Point]: 1, 20 | [ComputeRasterPrimitiveType.Line]: 2, 21 | [ComputeRasterPrimitiveType.Triangle]: 3, 22 | [ComputeRasterPrimitiveType.Quad]: 4, 23 | }; 24 | 25 | class ComputeRasterTask { 26 | static init(task, name, shader, points, connections, inputs, primitive_type) { 27 | if (!points || !(points instanceof Buffer)) { 28 | throw new Error("ComputeRasterTask requires a valid 'points' buffer."); 29 | } 30 | 31 | // Validate and determine stride from supported primitive types 32 | const stride = ComputeRasterPrimitiveStride[primitive_type]; 33 | if (stride === undefined) { 34 | throw new Error(`Unsupported or undefined primitive type: ${primitive_type}`); 35 | } 36 | 37 | // Determine the number of primitives (e.g. points) from the connections buffer 38 | let raw = connections.config.raw_data; 39 | let num_connections = ArrayBuffer.isView(raw) ? raw.length : 0; 40 | 41 | const num_primitives = Math.floor(num_connections / stride); 42 | const dispatch_count = Math.ceil(num_primitives / workgroup_size); 43 | 44 | task.name = name; 45 | task.shader = shader; 46 | task.points = points; 47 | task.connections = connections; 48 | task.inputs = inputs; 49 | task.primitive_type = primitive_type; 50 | task.stride = stride; 51 | task.num_primitives = num_primitives; 52 | task.dispatch_x = dispatch_count; 53 | task.dispatch_y = 1; 54 | task.dispatch_z = 1; 55 | task.intermediate_buffers = null; 56 | } 57 | } 58 | 59 | export class ComputeRasterTaskQueue { 60 | static tasks = []; 61 | static tasks_allocator = new RandomAccessAllocator(256, new ComputeRasterTask()); 62 | 63 | /** 64 | * Creates a new compute raster task. 65 | * @param {string} name - The name of the task. 66 | * @param {string} shader - The path to the compute shader. 67 | * @param {Buffer} points - The required points buffer (vertex buffer). 68 | * @param {Buffer} connections - The required connections buffer (index buffer). 69 | * @param {Buffer[]} inputs - The optional input buffers. 70 | * @param {string} primitiveType - The type of primitive to rasterize (e.g., 'point', 'line'). 71 | * @returns {ComputeRasterTask} The newly created task. 72 | */ 73 | static new_task(name, shader, points, connections, inputs, primitiveType) { 74 | const task = this.tasks_allocator.allocate(); 75 | 76 | ComputeRasterTask.init(task, name, shader, points, connections, inputs, primitiveType); 77 | 78 | this.tasks.push(task); 79 | 80 | return task; 81 | } 82 | 83 | static compile_rg_passes(render_graph, pipeline_outputs) { 84 | profile_scope(compile_rg_pass_scope_name, () => { 85 | if (this.tasks.length === 0) return; 86 | 87 | // For each task, bind outputs directly to textures 88 | for (let i = 0; i < this.tasks.length; i++) { 89 | const task = this.tasks[i]; 90 | // Register required buffers 91 | task.points = render_graph.register_buffer(task.points.config.name); 92 | task.connections = render_graph.register_buffer(task.connections.config.name); 93 | for (let k = 0; k < task.inputs.length; k++) { 94 | task.inputs[k] = render_graph.register_buffer(task.inputs[k].config.name); 95 | } 96 | 97 | // Add compute pass writing directly to G-Buffer textures 98 | render_graph.add_pass( 99 | task.name, 100 | RenderPassFlags.Compute, 101 | { 102 | shader_setup: { pipeline_shaders: { compute: { path: task.shader } } }, 103 | inputs: [task.points, task.connections, ...pipeline_outputs, ...task.inputs], 104 | outputs: pipeline_outputs, 105 | }, 106 | (graph, frame_data, encoder) => { 107 | const pass = graph.get_physical_pass(frame_data.current_pass); 108 | pass.dispatch(task.dispatch_x, task.dispatch_y, task.dispatch_z); 109 | } 110 | ); 111 | } 112 | // Clear tasks after dispatch 113 | this.reset(); 114 | }); 115 | } 116 | 117 | static reset() { 118 | this.tasks_allocator.reset(); 119 | this.tasks.length = 0; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /engine/src/renderer/compute_task_queue.js: -------------------------------------------------------------------------------- 1 | import { Buffer } from "./buffer.js"; 2 | import { Texture } from "./texture.js"; 3 | import { RandomAccessAllocator } from "../memory/allocator.js"; 4 | import { profile_scope } from "../utility/performance.js"; 5 | import { RenderPassFlags } from "./renderer_types.js"; 6 | 7 | class ComputeTask { 8 | static init( 9 | task, 10 | name, 11 | shader, 12 | inputs, 13 | outputs, 14 | dispatch_x, 15 | dispatch_y, 16 | dispatch_z 17 | ) { 18 | task.name = name; 19 | task.shader = shader; 20 | task.inputs = inputs; 21 | task.outputs = outputs; 22 | task.dispatch_x = dispatch_x; 23 | task.dispatch_y = dispatch_y; 24 | task.dispatch_z = dispatch_z; 25 | } 26 | } 27 | 28 | export class ComputeTaskQueue { 29 | static tasks = []; 30 | static tasks_allocator = new RandomAccessAllocator(256, new ComputeTask()); 31 | 32 | static new_task( 33 | name, 34 | shader, 35 | inputs, 36 | outputs, 37 | dispatch_x, 38 | dispatch_y = 1, 39 | dispatch_z = 1 40 | ) { 41 | const task = this.tasks_allocator.allocate(); 42 | 43 | ComputeTask.init( 44 | task, 45 | name, 46 | shader, 47 | inputs, 48 | outputs, 49 | dispatch_x, 50 | dispatch_y, 51 | dispatch_z 52 | ); 53 | 54 | this.tasks.push(task); 55 | 56 | return task; 57 | } 58 | 59 | static compile_rg_passes(render_graph) { 60 | profile_scope("ComputeTaskQueue.compile_rg_passes", () => { 61 | for (let i = 0; i < this.tasks.length; i++) { 62 | const task = this.tasks[i]; 63 | 64 | for (let j = 0; j < task.inputs.length; j++) { 65 | if (task.inputs[j] instanceof Buffer) { 66 | task.inputs[j] = render_graph.register_buffer(task.inputs[j].config.name); 67 | } else if (task.inputs[j] instanceof Texture) { 68 | task.inputs[j] = render_graph.register_texture(task.inputs[j].config.name); 69 | } 70 | } 71 | 72 | for (let j = 0; j < task.outputs.length; j++) { 73 | if (task.outputs[j] instanceof Buffer) { 74 | task.outputs[j] = render_graph.register_buffer(task.outputs[j].config.name); 75 | } else if (task.outputs[j] instanceof Texture) { 76 | task.outputs[j] = render_graph.register_texture(task.outputs[j].config.name); 77 | } 78 | } 79 | 80 | render_graph.add_pass( 81 | task.name, 82 | RenderPassFlags.Compute, 83 | { 84 | shader_setup: { pipeline_shaders: { compute: { path: task.shader } } }, 85 | inputs: task.inputs, 86 | outputs: task.outputs, 87 | }, 88 | (graph, frame_data, encoder) => { 89 | const pass = graph.get_physical_pass(frame_data.current_pass); 90 | pass.dispatch( 91 | task.dispatch_x, 92 | task.dispatch_y, 93 | task.dispatch_z 94 | ); 95 | } 96 | ); 97 | } 98 | }); 99 | } 100 | 101 | static reset() { 102 | this.tasks_allocator.reset(); 103 | this.tasks.length = 0; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /engine/src/renderer/pipeline_state.js: -------------------------------------------------------------------------------- 1 | import { Renderer } from "./renderer.js"; 2 | import { Name } from "../utility/names.js"; 3 | import { ResourceCache } from "./resource_cache.js"; 4 | import { CacheTypes } from "./renderer_types.js"; 5 | 6 | export class PipelineState { 7 | pipeline = null; 8 | layout = null; 9 | 10 | init_render_pipeline(name, config) { 11 | const renderer = Renderer.get(); 12 | 13 | if (config.bind_layouts && config.bind_layouts.length) { 14 | this.layout = renderer.device.createPipelineLayout({ 15 | label: Name.string(name), 16 | bindGroupLayouts: config.bind_layouts, 17 | }); 18 | } 19 | this.pipeline = renderer.device.createRenderPipeline({ 20 | label: Name.string(name), 21 | layout: this.layout ?? 'auto', 22 | ...config 23 | }); 24 | } 25 | 26 | init_compute_pipeline(name, config) { 27 | const renderer = Renderer.get(); 28 | 29 | if (config.bind_layouts && config.bind_layouts.length) { 30 | this.layout = renderer.device.createPipelineLayout({ 31 | label: Name.string(name), 32 | bindGroupLayouts: config.bind_layouts, 33 | }); 34 | } 35 | this.pipeline = renderer.device.createComputePipeline({ 36 | label: Name.string(name), 37 | layout: this.layout ?? 'auto', 38 | ...config 39 | }); 40 | } 41 | 42 | static create_render(name, config) { 43 | let name_hash = Name.from(name); 44 | let pipeline_state = ResourceCache.get().fetch(CacheTypes.PIPELINE_STATE, name_hash); 45 | if (!pipeline_state) { 46 | pipeline_state = new PipelineState(); 47 | pipeline_state.init_render_pipeline(name_hash, config); 48 | ResourceCache.get().store(CacheTypes.PIPELINE_STATE, name_hash, pipeline_state); 49 | } 50 | return pipeline_state; 51 | } 52 | 53 | static create_compute(name, config) { 54 | let name_hash = Name.from(name); 55 | let pipeline_state = ResourceCache.get().fetch(CacheTypes.PIPELINE_STATE, name_hash); 56 | if (!pipeline_state) { 57 | pipeline_state = new PipelineState(); 58 | pipeline_state.init_compute_pipeline(name_hash, config); 59 | ResourceCache.get().store(CacheTypes.PIPELINE_STATE, name_hash, pipeline_state); 60 | } 61 | return pipeline_state; 62 | } 63 | } -------------------------------------------------------------------------------- /engine/src/renderer/render_pass.js: -------------------------------------------------------------------------------- 1 | import { Name } from "../utility/names.js"; 2 | import { ResourceCache } from "./resource_cache.js"; 3 | import { RenderPassFlags, CacheTypes } from "./renderer_types.js"; 4 | 5 | export class RenderPass { 6 | pass = null; 7 | config = null; 8 | 9 | init(config) { 10 | this.config = config; 11 | } 12 | 13 | begin(encoder, pipeline) { 14 | if (this.config.flags & RenderPassFlags.Graphics) { 15 | const attachments = this.config.attachments 16 | .map((attachment) => { 17 | const image = ResourceCache.get().fetch( 18 | CacheTypes.IMAGE, 19 | attachment.image 20 | ); 21 | return { 22 | view: image.get_view(attachment.view_index) || image.view, 23 | clearValue: image.config.clear_value ?? { r: 0, g: 0, b: 0, a: 1 }, 24 | loadOp: image.config.load_op ?? 'clear', 25 | storeOp: image.config.store_op ?? 'store', 26 | }; 27 | }); 28 | 29 | const pass_desc = { 30 | label: this.config.name, 31 | colorAttachments: attachments, 32 | }; 33 | 34 | if (this.config.depth_stencil_attachment) { 35 | const depth_stencil_image = ResourceCache.get().fetch( 36 | CacheTypes.IMAGE, 37 | this.config.depth_stencil_attachment.image 38 | ); 39 | pass_desc.depthStencilAttachment = { 40 | view: depth_stencil_image.get_view(this.config.depth_stencil_attachment.view_index) || depth_stencil_image.view, 41 | depthClearValue: depth_stencil_image.config.clear_value ?? 0.0, 42 | depthLoadOp: depth_stencil_image.config.load_op ?? "load", 43 | depthStoreOp: depth_stencil_image.config.store_op ?? "store", 44 | }; 45 | } 46 | 47 | this.pass = encoder.beginRenderPass(pass_desc); 48 | } else if (this.config.flags & RenderPassFlags.Compute) { 49 | this.pass = encoder.beginComputePass({ 50 | label: this.config.name, 51 | }); 52 | } 53 | 54 | if (this.config.viewport) { 55 | this.pass.setViewport(this.config.viewport); 56 | } 57 | if (this.config.scissor_rect) { 58 | this.pass.setScissorRect(this.config.scissor_rect); 59 | } 60 | if (this.config.vertex_buffer) { 61 | this.pass.setVertexBuffer(this.config.vertex_buffer); 62 | } 63 | if (this.config.index_buffer) { 64 | this.pass.setIndexBuffer(this.config.index_buffer); 65 | } 66 | 67 | if (pipeline) { 68 | this.set_pipeline(pipeline); 69 | } 70 | } 71 | 72 | set_pipeline(pipeline) { 73 | this.pass.setPipeline(pipeline.pipeline); 74 | } 75 | 76 | set_attachments(attachments) { 77 | this.config.attachments = attachments; 78 | } 79 | 80 | set_depth_stencil_attachment(attachment) { 81 | this.config.depth_stencil_attachment = attachment; 82 | } 83 | 84 | dispatch(x, y, z) { 85 | if (this.config.flags & RenderPassFlags.Compute) { 86 | this.pass.dispatchWorkgroups(x, y, z); 87 | } 88 | } 89 | 90 | end() { 91 | if (this.pass) { 92 | this.pass.end(); 93 | } 94 | } 95 | 96 | static create(config) { 97 | let render_pass = ResourceCache.get().fetch( 98 | CacheTypes.PASS, 99 | Name.from(config.name) 100 | ); 101 | if (!render_pass) { 102 | render_pass = new RenderPass(); 103 | render_pass.init(config); 104 | ResourceCache.get().store( 105 | CacheTypes.PASS, 106 | Name.from(config.name), 107 | render_pass 108 | ); 109 | } 110 | return render_pass; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /engine/src/renderer/renderer_types.js: -------------------------------------------------------------------------------- 1 | const graphics_pass_name = "Graphics"; 2 | const present_pass_name = "Present"; 3 | const compute_pass_name = "Compute"; 4 | const graph_local_pass_name = "GraphLocal"; 5 | 6 | /** 7 | * Flags for image resources in the render graph. 8 | * @enum {number} 9 | */ 10 | export const ImageFlags = Object.freeze({ 11 | /** No flags */ 12 | None: 0, 13 | /** Indicates a transient image resource */ 14 | Transient: 1, 15 | /** Indicates the image is loaded locally */ 16 | LocalLoad: 2, 17 | }); 18 | 19 | /** 20 | * Types of shader resources. 21 | * @enum {number} 22 | */ 23 | export const ShaderResourceType = Object.freeze({ 24 | Uniform: 0, 25 | Storage: 1, 26 | Texture: 2, 27 | Sampler: 3, 28 | StorageTexture: 4, 29 | }); 30 | 31 | /** 32 | * Types of resources in the resource cache. 33 | * @enum {number} 34 | */ 35 | export const CacheTypes = Object.freeze({ 36 | SHADER: 0, 37 | PIPELINE_STATE: 1, 38 | RENDER_PASS: 2, 39 | BIND_GROUP: 3, 40 | BIND_GROUP_LAYOUT: 4, 41 | BUFFER: 5, 42 | IMAGE: 6, 43 | SAMPLER: 7, 44 | MESH: 8, 45 | MATERIAL: 9, 46 | }); 47 | 48 | /** 49 | * Flags for render passes in the render graph. 50 | * @enum {number} 51 | */ 52 | export const RenderPassFlags = Object.freeze({ 53 | /** No flags */ 54 | None: 0, 55 | /** Indicates a graphics pass */ 56 | Graphics: 1, 57 | /** Indicates a present pass */ 58 | Present: 2, 59 | /** Indicates a compute pass */ 60 | Compute: 4, 61 | /** Indicates a graph-local pass */ 62 | GraphLocal: 8, 63 | }); 64 | 65 | /** 66 | * Types of material families. 67 | * @enum {number} 68 | */ 69 | export const MaterialFamilyType = Object.freeze({ 70 | Opaque: 0, 71 | Transparent: 1, 72 | }); 73 | 74 | /** 75 | * Flags for buffer resources in the render graph. 76 | * @enum {number} 77 | */ 78 | export const BufferFlags = Object.freeze({ 79 | /** No flags */ 80 | None: 0, 81 | /** Indicates a transient buffer resource */ 82 | Transient: 1, 83 | }); 84 | 85 | /** 86 | * Index of bindless group for image resources. 87 | * @enum {number} 88 | */ 89 | export const BindlessGroupIndex = Object.freeze({ 90 | Image: 0, 91 | StorageImage: 1 92 | }); 93 | 94 | /** 95 | * Types of bind groups in the render graph. 96 | * @enum {number} 97 | */ 98 | export const BindGroupType = Object.freeze({ 99 | Global: 0, 100 | Pass: 1, 101 | Material: 2, 102 | Num: 3 103 | }); 104 | 105 | /** 106 | * Converts render pass flags to a string. 107 | * @param {number} flags - The flags to convert. 108 | * @returns {string} The string representation of the flags. 109 | */ 110 | export function render_pass_flags_to_string(flags) { 111 | const flag_names = []; 112 | if (flags & RenderPassFlags.Graphics) flag_names.push(graphics_pass_name); 113 | if (flags & RenderPassFlags.Present) flag_names.push(present_pass_name); 114 | if (flags & RenderPassFlags.Compute) flag_names.push(compute_pass_name); 115 | if (flags & RenderPassFlags.GraphLocal) flag_names.push(graph_local_pass_name); 116 | return flag_names.join(", "); 117 | } 118 | -------------------------------------------------------------------------------- /engine/src/renderer/resource_cache.js: -------------------------------------------------------------------------------- 1 | import { CacheTypes } from "./renderer_types.js"; 2 | 3 | export class ResourceCache { 4 | constructor() { 5 | if (ResourceCache.instance) { 6 | return ResourceCache.instance; 7 | } 8 | ResourceCache.instance = this; 9 | 10 | this.cache = new Map(); 11 | this.cache.set(CacheTypes.SHADER, new Map()); 12 | this.cache.set(CacheTypes.PIPELINE_STATE, new Map()); 13 | this.cache.set(CacheTypes.PASS, new Map()); 14 | this.cache.set(CacheTypes.BIND_GROUP, new Map()); 15 | this.cache.set(CacheTypes.BIND_GROUP_LAYOUT, new Map()); 16 | this.cache.set(CacheTypes.BUFFER, new Map()); 17 | this.cache.set(CacheTypes.IMAGE, new Map()); 18 | this.cache.set(CacheTypes.SAMPLER, new Map()); 19 | this.cache.set(CacheTypes.MESH, new Map()); 20 | this.cache.set(CacheTypes.MATERIAL, new Map()); 21 | } 22 | 23 | static get() { 24 | if (!ResourceCache.instance) { 25 | ResourceCache.instance = new ResourceCache() 26 | } 27 | return ResourceCache.instance; 28 | } 29 | 30 | fetch(type, key) { 31 | return this.cache.get(type).get(key); 32 | } 33 | 34 | store(type, key, value) { 35 | this.cache.get(type).set(key, value); 36 | } 37 | 38 | remove(type, key) { 39 | this.cache.get(type).delete(key); 40 | } 41 | 42 | size(type) { 43 | return this.cache.get(type).size; 44 | } 45 | 46 | flush(type) { 47 | const keys = this.cache.get(type).keys(); 48 | for (const key of keys) { 49 | const resource = this.cache.get(type).get(key); 50 | resource.destroy?.(); 51 | this.cache.get(type).delete(key); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /engine/src/tools/camera_info.js: -------------------------------------------------------------------------------- 1 | import { DevConsoleTool } from "./dev_console_tool.js"; 2 | import { SharedViewBuffer } from "../core/shared_data.js"; 3 | import { InputProvider } from "../input/input_provider.js"; 4 | import { InputKey } from "../input/input_types.js"; 5 | import { panel, label } from "../ui/2d/immediate.js"; 6 | 7 | /* 8 | Panel and label configurations adapted for immediate mode UI. 9 | Adjust these values as needed. 10 | */ 11 | const stats_panel_config = { 12 | layout: "column", 13 | gap: 4, 14 | y: 25, 15 | x: 25, 16 | anchor_x: "right", 17 | dont_consume_cursor_events: true, 18 | background_color: "rgba(0, 0, 0, 0.7)", 19 | width: 600, 20 | padding: 10, 21 | border: "1px solid rgb(68, 68, 68)", 22 | corner_radius: 5, 23 | }; 24 | 25 | const stats_label_config = { 26 | text_color: "#fff", 27 | wrap: true, 28 | font: "16px monospace", 29 | width: "100%", 30 | height: "fit-content", 31 | text_valign: "middle", 32 | text_align: "left", 33 | text_padding: 5, 34 | }; 35 | 36 | /** 37 | * CameraInfo displays camera information using the 38 | * immediate mode UI framework. 39 | */ 40 | export class CameraInfo extends DevConsoleTool { 41 | is_open = false; 42 | scene = null; 43 | 44 | /** 45 | * Called each frame to update the MLStats state. If the user clicks 46 | * outside the stats panel, the panel is hidden. 47 | */ 48 | update(delta_time) { 49 | if (!this.is_open) return; 50 | this.render(); 51 | } 52 | 53 | /** 54 | * Renders the camera info panel. 55 | * This method should be called every frame as part of the render loop. 56 | */ 57 | render() { 58 | let panel_state = panel(stats_panel_config, () => { 59 | const count = SharedViewBuffer.get_view_data_count(); 60 | for (let i = 0; i < count; i++) { 61 | const view_data = SharedViewBuffer.get_view_data(i); 62 | label( 63 | `Camera ${i} Position: ${view_data.position[0]}, ${view_data.position[1]}, ${view_data.position[2]}`, 64 | stats_label_config 65 | ); 66 | label( 67 | `Camera ${i} Rotation: ${view_data.rotation[0]}, ${view_data.rotation[1]}, ${view_data.rotation[2]}, ${view_data.rotation[3]}`, 68 | stats_label_config 69 | ); 70 | label(`Camera ${i} FOV: ${view_data.fov}`, stats_label_config); 71 | label(`Camera ${i} Near: ${view_data.near}`, stats_label_config); 72 | label(`Camera ${i} Far: ${view_data.far}`, stats_label_config); 73 | } 74 | }); 75 | 76 | if (this.is_open && InputProvider.get_action(InputKey.B_mouse_left)) { 77 | if (!panel_state.hovered) { 78 | this.hide(); 79 | } 80 | InputProvider.consume_action(InputKey.B_mouse_left); 81 | } 82 | } 83 | 84 | /** 85 | * Toggles the display of the ML stats panel. 86 | */ 87 | execute() { 88 | this.toggle(); 89 | } 90 | 91 | toggle() { 92 | this.is_open = !this.is_open; 93 | if (this.scene) { 94 | if (this.is_open && typeof this.scene.show_dev_cursor === "function") { 95 | this.scene.show_dev_cursor(); 96 | } else if (!this.is_open && typeof this.scene.hide_dev_cursor === "function") { 97 | this.scene.hide_dev_cursor(); 98 | } 99 | } 100 | } 101 | 102 | show() { 103 | this.is_open = true; 104 | if (this.scene && typeof this.scene.show_dev_cursor === "function") { 105 | this.scene.show_dev_cursor(); 106 | } 107 | } 108 | 109 | hide() { 110 | this.is_open = false; 111 | if (this.scene && typeof this.scene.hide_dev_cursor === "function") { 112 | this.scene.hide_dev_cursor(); 113 | } 114 | } 115 | 116 | set_scene(scene) { 117 | this.scene = scene; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /engine/src/tools/dev_console_tool.js: -------------------------------------------------------------------------------- 1 | export class DevConsoleTool { 2 | is_open = false; 3 | 4 | init() {} 5 | update() {} 6 | execute() {} 7 | show() { 8 | this.is_open = true; 9 | } 10 | hide() { 11 | this.is_open = false; 12 | } 13 | toggle() { 14 | if (this.is_open) { 15 | this.hide(); 16 | } else { 17 | this.show(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /engine/src/tools/ml_stats.js: -------------------------------------------------------------------------------- 1 | import { DevConsoleTool } from "./dev_console_tool.js"; 2 | import { MasterMind } from "../ml/mastermind.js"; 3 | import { InputProvider } from "../input/input_provider.js"; 4 | import { InputKey } from "../input/input_types.js"; 5 | import { panel, label } from "../ui/2d/immediate.js"; 6 | 7 | /* 8 | Panel and label configurations adapted for immediate mode UI. 9 | Adjust these values as needed. 10 | */ 11 | const stats_panel_config = { 12 | layout: "column", 13 | gap: 4, 14 | y: 25, 15 | x: 25, 16 | anchor_x: "right", 17 | dont_consume_cursor_events: true, 18 | background_color: "rgba(0, 0, 0, 0.7)", 19 | width: 600, 20 | padding: 10, 21 | border: "1px solid rgb(68, 68, 68)", 22 | corner_radius: 5, 23 | }; 24 | 25 | const stats_label_config = { 26 | text_color: "#fff", 27 | font: "16px monospace", 28 | height: 20, 29 | width: "100%", 30 | text_valign: "middle", 31 | text_align: "left", 32 | text_padding: 5, 33 | }; 34 | 35 | /** 36 | * MLStats displays machine learning model statistics using the 37 | * immediate mode UI framework. 38 | */ 39 | export class MLStats extends DevConsoleTool { 40 | is_open = false; 41 | scene = null; 42 | 43 | /** 44 | * Called each frame to update the MLStats state. If the user clicks 45 | * outside the stats panel, the panel is hidden. 46 | */ 47 | update(delta_time) { 48 | if (!this.is_open) return; 49 | this.render(); 50 | } 51 | 52 | /** 53 | * Renders the ML statistics panel. 54 | * This method should be called every frame as part of the render loop. 55 | */ 56 | render() { 57 | let panel_state = panel(stats_panel_config, 58 | () => { 59 | // Gather the aggregated stats from all MasterMind instances. 60 | const all_masterminds = MasterMind.all_masterminds; 61 | let all_stats = []; 62 | for (let i = 0; i < all_masterminds.length; i++) { 63 | const mastermind = all_masterminds[i]; 64 | const stats = mastermind.get_subnet_stats(); 65 | all_stats.push(...stats); 66 | } 67 | 68 | // For each model, render one label per statistic. 69 | for (let i = 0; i < all_stats.length; i++) { 70 | const entry = all_stats[i]; 71 | const subnet_name = entry.name || `subnet_${i}`; 72 | const subnet_stats = entry.stats; 73 | for (let j = 0; j < subnet_stats.length; j++) { 74 | const stat = subnet_stats[j]; 75 | if (stat && stat.loss) { 76 | label( 77 | `${subnet_name} - ${stat.name}: ${stat.loss ? stat.loss.data[0] : "N/A"}`, 78 | stats_label_config 79 | ); 80 | } 81 | } 82 | } 83 | } 84 | ); 85 | 86 | if (this.is_open && InputProvider.get_action(InputKey.B_mouse_left)) { 87 | if (!panel_state.hovered) { 88 | this.hide(); 89 | } 90 | InputProvider.consume_action(InputKey.B_mouse_left); 91 | } 92 | } 93 | 94 | /** 95 | * Toggles the display of the ML stats panel. 96 | */ 97 | execute() { 98 | this.toggle(); 99 | } 100 | 101 | toggle() { 102 | this.is_open = !this.is_open; 103 | if (this.scene) { 104 | if (this.is_open && typeof this.scene.show_dev_cursor === "function") { 105 | this.scene.show_dev_cursor(); 106 | } else if (!this.is_open && typeof this.scene.hide_dev_cursor === "function") { 107 | this.scene.hide_dev_cursor(); 108 | } 109 | } 110 | } 111 | 112 | show() { 113 | this.is_open = true; 114 | if (this.scene && typeof this.scene.show_dev_cursor === "function") { 115 | this.scene.show_dev_cursor(); 116 | } 117 | } 118 | 119 | hide() { 120 | this.is_open = false; 121 | if (this.scene && typeof this.scene.hide_dev_cursor === "function") { 122 | this.scene.hide_dev_cursor(); 123 | } 124 | } 125 | 126 | set_scene(scene) { 127 | this.scene = scene; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /engine/src/tools/performance_trace.js: -------------------------------------------------------------------------------- 1 | import { DevConsoleTool } from "./dev_console_tool.js"; 2 | import { panel, label } from "../ui/2d/immediate.js"; 3 | import { is_trace_activated, set_trace_activated } from "../utility/performance.js"; 4 | 5 | /* 6 | Panel and label configurations adapted for immediate mode UI. 7 | Adjust these values as needed. 8 | */ 9 | const trace_panel_config = { 10 | layout: "column", 11 | gap: 4, 12 | y: 25, 13 | x: 25, 14 | anchor_x: "right", 15 | dont_consume_cursor_events: true, 16 | background_color: "rgba(98, 16, 16, 0.7)", 17 | padding: 10, 18 | width: 300, 19 | border: "1px solid rgb(68, 68, 68)", 20 | corner_radius: 5, 21 | }; 22 | 23 | const trace_label_config = { 24 | text_color: "#fff", 25 | font: "16px monospace", 26 | height: 20, 27 | width: "fit-content", 28 | text_valign: "middle", 29 | text_align: "left", 30 | text_padding: 5, 31 | }; 32 | 33 | /** 34 | * PerformanceTrace displays performance trace information using the 35 | * immediate mode UI framework. 36 | */ 37 | export class PerformanceTrace extends DevConsoleTool { 38 | scene = null; 39 | 40 | /** 41 | * Called each frame to update the PerformanceTrace state. If the user clicks 42 | * outside the trace panel, the panel is hidden. 43 | */ 44 | update(delta_time) { 45 | if (!is_trace_activated()) return; 46 | this.render(); 47 | } 48 | 49 | /** 50 | * Renders the performance trace panel. 51 | * This method should be called every frame as part of the render loop. 52 | */ 53 | render() { 54 | if (!is_trace_activated()) return; 55 | 56 | panel(trace_panel_config, 57 | () => { 58 | label("Performance Tracing...", trace_label_config); 59 | } 60 | ); 61 | } 62 | 63 | /** 64 | * Toggles the display of the performance trace panel. 65 | */ 66 | execute() { 67 | this.toggle(); 68 | } 69 | 70 | /** 71 | * Toggles the display of the performance trace panel. 72 | */ 73 | toggle() { 74 | const is_tracing = is_trace_activated(); 75 | set_trace_activated(!is_tracing); 76 | } 77 | 78 | /** 79 | * Sets the scene for the performance trace tool. 80 | * @param {Object} scene - The scene to set. 81 | */ 82 | set_scene(scene) { 83 | this.scene = scene; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /engine/src/ui/text/font_cache.js: -------------------------------------------------------------------------------- 1 | import { Renderer } from "../../renderer/renderer.js"; 2 | import { Font } from "./font.js"; 3 | import { read_file } from "../../utility/file_system.js"; 4 | import { Name } from "../../utility/names.js"; 5 | 6 | const json_extension = ".json"; 7 | 8 | export class FontCache { 9 | static cache = new Map(); 10 | static font_paths = ['engine/fonts']; 11 | 12 | /** 13 | * Registers a new font path to be scanned for fonts. 14 | * @param {string} path - The path to the directory containing font files 15 | */ 16 | static register_font_path(path) { 17 | this.font_paths.push(path); 18 | } 19 | 20 | /** 21 | * Gets the font ID for a given font name 22 | * @param {string} font_name - The name of the font 23 | * @param {string|null} font_path - Optional path to font file 24 | * @returns {number|null} The font ID or null if font couldn't be loaded 25 | */ 26 | static get_font(font_name, font_path = null) { 27 | // Return existing ID if font is already loaded 28 | const font_id = Name.from(font_name); 29 | if (this.cache.has(font_id)) { 30 | return font_id; 31 | } 32 | 33 | const font = Font.create(font_path); 34 | if (!font) return null; 35 | 36 | this.cache.set(font_id, font); 37 | return font_id; 38 | } 39 | 40 | /** 41 | * Gets the font object for a given font ID 42 | * @param {number} font_id - The ID of the font 43 | * @returns {Font|null} The font object or null if not found 44 | */ 45 | static get_font_object(font_id) { 46 | return this.cache.get(font_id) || null; 47 | } 48 | 49 | /** 50 | * Automatically loads all fonts from registered font paths by scanning their font manifests. 51 | * Iterates through each registered font path and calls scan_directory() to load fonts. 52 | */ 53 | static auto_load_fonts() { 54 | for (const path of this.font_paths) { 55 | this.scan_directory(path); 56 | } 57 | } 58 | 59 | /** 60 | * Scans a directory for font manifests and loads all fonts found. 61 | * @param {string} path - The path to the directory to scan 62 | */ 63 | static scan_directory(path) { 64 | const manifest = JSON.parse(read_file(`${path}/font_manifest.json`)); 65 | 66 | if (!manifest) return; 67 | 68 | for (const entry of manifest.fonts) { 69 | if (this.is_valid_font_data_file(entry.path)) { 70 | this.get_font(entry.name, entry.path); 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * Checks if a given file path is a valid font data file. 77 | * @param {string} filepath - The path to the file to check 78 | * @returns {boolean} True if the file is a valid font data file, false otherwise 79 | */ 80 | static is_valid_font_data_file(filepath) { 81 | return filepath.toLowerCase().endsWith(json_extension); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /engine/src/utility/color.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates a random RGB color. 3 | * @returns {Object} An object with r, g, and b properties, each ranging from 0 to 255. 4 | */ 5 | export function random_rgb() { 6 | return { 7 | r: Math.floor(Math.random() * 256), 8 | g: Math.floor(Math.random() * 256), 9 | b: Math.floor(Math.random() * 256) 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /engine/src/utility/config_permutations.js: -------------------------------------------------------------------------------- 1 | export const no_cull_rasterizer_config = { 2 | rasterizer_state: { 3 | cull_mode: "none", 4 | }, 5 | }; 6 | 7 | export const one_one_blend_config = { 8 | color: { 9 | srcFactor: "one", 10 | dstFactor: "one", 11 | }, 12 | alpha: { 13 | srcFactor: "one", 14 | dstFactor: "one", 15 | }, 16 | }; 17 | 18 | export const zero_one_minus_src_blend_config = { 19 | color: { 20 | srcFactor: "zero", 21 | dstFactor: "one-minus-src", 22 | }, 23 | alpha: { 24 | srcFactor: "zero", 25 | dstFactor: "one-minus-src", 26 | }, 27 | }; 28 | 29 | export const src_alpha_one_minus_src_alpha_blend_config = { 30 | color: { 31 | srcFactor: "src-alpha", 32 | dstFactor: "one-minus-src-alpha", 33 | }, 34 | alpha: { 35 | srcFactor: "src-alpha", 36 | dstFactor: "one-minus-src-alpha", 37 | }, 38 | }; 39 | 40 | export const rgba32float_format = "rgba32float"; 41 | export const rgba16float_format = "rgba16float"; 42 | export const rgba32uint_format = "rgba32uint"; 43 | export const rgba32sint_format = "rgba32sint"; 44 | export const r8unorm_format = "r8unorm"; 45 | export const depth32float_format = "depth32float"; 46 | export const depth24plus_format = "depth24plus"; 47 | export const bgra8unorm_format = "bgra8unorm"; 48 | export const rgba8unorm_format = "rgba8unorm"; 49 | export const rgba8unorm_srgb_format = "rgba8unorm-srgb"; 50 | export const rgba8snorm_format = "rgba8snorm"; 51 | export const rgba8uint_format = "rgba8uint"; 52 | export const rgba8sint_format = "rgba8sint"; 53 | export const rg32float_format = "rg32float"; 54 | export const r32float_format = "r32float"; 55 | export const r16float_format = "r16float"; 56 | export const r32uint_format = "r32uint"; 57 | export const rg32uint_format = "rg32uint"; 58 | export const r32sint_format = "r32sint"; 59 | export const r16uint_format = "r16uint"; 60 | export const r16sint_format = "r16sint"; 61 | export const r11b11g10float_format = "rg11b10ufloat"; 62 | 63 | export const load_op_clear = "clear"; 64 | export const load_op_load = "load"; 65 | export const load_op_dont_care = "dont-care"; -------------------------------------------------------------------------------- /engine/src/utility/execution_queue.js: -------------------------------------------------------------------------------- 1 | export default class ExecutionQueue { 2 | constructor() { 3 | this.executions = []; 4 | this.execution_ids = []; 5 | this.execution_delays = []; 6 | } 7 | 8 | push_execution(execution, execution_id, execution_frame_delay = 0) { 9 | this.executions.push(execution); 10 | this.execution_ids.push(execution_id); 11 | this.execution_delays.push(execution_frame_delay); 12 | } 13 | 14 | remove_execution(execution) { 15 | const index = this.executions.indexOf(execution); 16 | if (index !== -1) { 17 | this.executions.splice(index, 1); 18 | this.execution_delays.splice(index, 1); 19 | this.execution_ids.splice(index, 1); 20 | } 21 | } 22 | 23 | update() { 24 | for (let i = this.executions.length - 1; i >= 0; --i) { 25 | if (--this.execution_delays[i] < 0) { 26 | this.executions[i](); 27 | this.executions.splice(i, 1); 28 | this.execution_delays.splice(i, 1); 29 | this.execution_ids.splice(i, 1); 30 | } 31 | } 32 | } 33 | 34 | flush() { 35 | for (let i = this.executions.length - 1; i >= 0; --i) { 36 | this.executions[i](); 37 | } 38 | this.executions = []; 39 | this.execution_delays = []; 40 | this.execution_ids = []; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /engine/src/utility/file_system.js: -------------------------------------------------------------------------------- 1 | export function write_file(file_path, content) { 2 | const url = new URL(`${file_path}`, window.location.href); 3 | const xhr = new XMLHttpRequest(); 4 | xhr.open('PUT', url.href, false); 5 | xhr.send(content); 6 | } 7 | 8 | export function read_file(file_path) { 9 | let asset = null; 10 | try { 11 | const url = new URL(`${file_path}`, window.location.href); 12 | 13 | // Check if file exists 14 | const check_xhr = new XMLHttpRequest(); 15 | check_xhr.open('HEAD', url.href, false); 16 | check_xhr.send(null); 17 | 18 | if (check_xhr.status === 200) { 19 | // File exists, now fetch its contents 20 | const get_xhr = new XMLHttpRequest(); 21 | get_xhr.open('GET', url.href, false); 22 | get_xhr.send(null); 23 | 24 | if (get_xhr.status === 200 && !get_xhr.responseText.includes("")) { 25 | asset = get_xhr.responseText; 26 | } 27 | } 28 | } catch (error) { 29 | // Network error or other issues, continue silently. Let caller handle null asset return. 30 | } 31 | return asset; 32 | } -------------------------------------------------------------------------------- /engine/src/utility/frame_runner.js: -------------------------------------------------------------------------------- 1 | export function frame_runner(frame_callback, max_fps = 60) { 2 | const fps = 1000 / (max_fps || 60); 3 | 4 | var current_time = 0; 5 | var delta_time = 0; 6 | var previous_time = performance.now(); 7 | 8 | return (async function loop() { 9 | current_time = performance.now(); 10 | delta_time = current_time - previous_time; 11 | 12 | if (delta_time > fps) { 13 | previous_time = current_time - (delta_time % fps); 14 | await frame_callback(delta_time / 1000.0); 15 | } 16 | 17 | requestAnimationFrame(loop); 18 | })(); 19 | } 20 | -------------------------------------------------------------------------------- /engine/src/utility/hashing.js: -------------------------------------------------------------------------------- 1 | export function hash_data(data_map, initial_hash) { 2 | let hash = initial_hash; 3 | for (const [key, value] of data_map) { 4 | hash = (hash << 5) - hash + hash_value(key); 5 | hash = (hash << 5) - hash + hash_value(value); 6 | hash |= 0; // Convert to 32-bit integer 7 | } 8 | return hash; 9 | } 10 | 11 | export function hash_value(value) { 12 | if (typeof value === "number") { 13 | return value; 14 | } else if (typeof value === "string") { 15 | return value.split("").reduce((acc, char) => { 16 | return (acc << 5) - acc + char.charCodeAt(0); 17 | }, 0); 18 | } else if (typeof value === "object" && value.config && value.config.name) { 19 | return value.config.name.split("").reduce((acc, char) => { 20 | return (acc << 5) - acc + char.charCodeAt(0); 21 | }, 0); 22 | } else { 23 | return 0; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /engine/src/utility/linear.js: -------------------------------------------------------------------------------- 1 | const EPSILON = 0.000001; 2 | 3 | /** 4 | * Returns an euler angle representation of a quaternion, in degrees 5 | * @param {vec3} out Euler angles, pitch-yaw-roll 6 | * @param {quat} mat Quaternion 7 | * @return {vec3} out 8 | */ 9 | export function quat_to_euler(out, q) { 10 | // Assuming q is in the form [x, y, z, w] 11 | const [x, y, z, w] = q; 12 | 13 | // Calculate pitch (x-axis rotation) 14 | const sinp = 2.0 * (w * y - z * x); 15 | if (Math.abs(sinp) >= 1) { 16 | out[1] = Math.copySign(Math.PI / 2, sinp); // use 90 degrees if out of range 17 | } else { 18 | out[0] = Math.asin(sinp); 19 | } 20 | 21 | // Calculate yaw (y-axis rotation) 22 | const siny_cosp = 2.0 * (w * z + x * y); 23 | const cosy_cosp = 1.0 - 2.0 * (y * y + z * z); 24 | out[1] = Math.atan2(siny_cosp, cosy_cosp); 25 | 26 | // Calculate roll (z-axis rotation) 27 | const sinr_cosp = 2.0 * (w * x + y * z); 28 | const cosr_cosp = 1.0 - 2.0 * (x * x + y * y); 29 | out[2] = Math.atan2(sinr_cosp, cosr_cosp); 30 | 31 | // Convert to degrees 32 | out[0] *= (180 / Math.PI); 33 | out[1] *= (180 / Math.PI); 34 | out[2] *= (180 / Math.PI); 35 | 36 | return out; // [pitch, yaw, roll] in degrees 37 | } 38 | 39 | export function direction_vector_to_quat(vec) { 40 | // This assumes vec is normalized 41 | const up = [0, 1, 0]; 42 | const right = [0, 0, 0]; 43 | const forward = [-vec[0], -vec[1], -vec[2]]; 44 | 45 | // Calculate right vector using cross product of up and forward 46 | right[0] = up[1] * forward[2] - up[2] * forward[1]; 47 | right[1] = up[2] * forward[0] - up[0] * forward[2]; 48 | right[2] = up[0] * forward[1] - up[1] * forward[0]; 49 | 50 | // Normalize right vector 51 | const right_length = Math.sqrt(right[0] * right[0] + right[1] * right[1] + right[2] * right[2]); 52 | right[0] /= right_length; 53 | right[1] /= right_length; 54 | right[2] /= right_length; 55 | 56 | // Recalculate up vector to ensure orthogonality 57 | up[0] = forward[1] * right[2] - forward[2] * right[1]; 58 | up[1] = forward[2] * right[0] - forward[0] * right[2]; 59 | up[2] = forward[0] * right[1] - forward[1] * right[0]; 60 | 61 | // Convert to quaternion (using rotation matrix to quaternion conversion) 62 | const trace = right[0] + up[1] + forward[2]; 63 | let qw, qx, qy, qz; 64 | 65 | if (trace > 0) { 66 | const S = Math.sqrt(trace + 1.0) * 2; 67 | qw = 0.25 * S; 68 | qx = (up[2] - forward[1]) / S; 69 | qy = (forward[0] - right[2]) / S; 70 | qz = (right[1] - up[0]) / S; 71 | } else if (right[0] > up[1] && right[0] > forward[2]) { 72 | const S = Math.sqrt(1.0 + right[0] - up[1] - forward[2]) * 2; 73 | qw = (up[2] - forward[1]) / S; 74 | qx = 0.25 * S; 75 | qy = (right[1] + up[0]) / S; 76 | qz = (right[2] + forward[0]) / S; 77 | } else if (up[1] > forward[2]) { 78 | const S = Math.sqrt(1.0 + up[1] - right[0] - forward[2]) * 2; 79 | qw = (forward[0] - right[2]) / S; 80 | qx = (right[1] + up[0]) / S; 81 | qy = 0.25 * S; 82 | qz = (up[2] + forward[1]) / S; 83 | } else { 84 | const S = Math.sqrt(1.0 + forward[2] - right[0] - up[1]) * 2; 85 | qw = (right[1] - up[0]) / S; 86 | qx = (right[2] + forward[0]) / S; 87 | qy = (up[2] + forward[1]) / S; 88 | qz = 0.25 * S; 89 | } 90 | 91 | return [qx, qy, qz, qw]; 92 | } 93 | 94 | export function is_vec_nearly_zero(vec, epsilon = EPSILON) { 95 | return Math.abs(vec[0]) < epsilon && Math.abs(vec[1]) < epsilon && Math.abs(vec[2]) < epsilon; 96 | } 97 | 98 | -------------------------------------------------------------------------------- /engine/src/utility/logging.js: -------------------------------------------------------------------------------- 1 | const noop = () => {}; 2 | 3 | const VERBOSITY_LEVELS = { 4 | SILENT: 0, 5 | ERROR: 1, 6 | WARN: 2, 7 | INFO: 3, 8 | DEBUG: 4, 9 | }; 10 | 11 | const VERBOSITY = VERBOSITY_LEVELS.INFO; 12 | 13 | export const log = __DEV__ && VERBOSITY >= VERBOSITY_LEVELS.INFO ? (...args) => console.log(...args) : noop; 14 | export const warn = __DEV__ && VERBOSITY >= VERBOSITY_LEVELS.WARN ? (...args) => console.warn(...args) : noop; 15 | export const error = __DEV__ && VERBOSITY >= VERBOSITY_LEVELS.ERROR ? (...args) => console.error(...args) : noop; 16 | 17 | 18 | -------------------------------------------------------------------------------- /engine/src/utility/names.js: -------------------------------------------------------------------------------- 1 | export class Name { 2 | static string_to_hash = new Map(); 3 | static hash_to_string = new Map(); 4 | 5 | constructor(str) { 6 | if (Name.string_to_hash.has(str)) { 7 | this.hash = Name.string_to_hash.get(str); 8 | } else { 9 | this.hash = Name.fnv1a_hash(str); 10 | Name.string_to_hash.set(str, this.hash); 11 | Name.hash_to_string.set(this.hash, str); 12 | } 13 | } 14 | 15 | static fnv1a_hash(str) { 16 | let hash = 0x811c9dc5; 17 | for (let i = 0; i < str.length; i++) { 18 | hash ^= str.charCodeAt(i); 19 | hash = (hash * 0x01000193) >>> 0; // Force 32-bit unsigned integer 20 | } 21 | return hash; 22 | } 23 | 24 | static string(hash) { 25 | return Name.hash_to_string.get(hash); 26 | } 27 | 28 | static hash(str) { 29 | return Name.string_to_hash.get(str); 30 | } 31 | 32 | static from(str) { 33 | const name = new Name(str); 34 | return name.hash; 35 | } 36 | } -------------------------------------------------------------------------------- /engine/src/utility/object.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Performs a deep clone of a JavaScript object/array/value 3 | * @param {*} value - The value to clone 4 | * @returns {*} - The cloned value 5 | */ 6 | export function deep_clone(value) { 7 | // Handle primitive types and functions 8 | if (typeof value !== 'object' || value === null) { 9 | return value; 10 | } 11 | 12 | // Handle Date objects 13 | if (value instanceof Date) { 14 | return new Date(value.getTime()); 15 | } 16 | 17 | // Handle ArrayBuffer 18 | if (value instanceof ArrayBuffer) { 19 | const copy = new ArrayBuffer(value.byteLength); 20 | new Uint8Array(copy).set(new Uint8Array(value)); 21 | return copy; 22 | } 23 | 24 | // Handle TypedArrays 25 | if (ArrayBuffer.isView(value)) { 26 | return value.slice(); 27 | } 28 | 29 | // Handle RegExp objects 30 | if (value instanceof RegExp) { 31 | return new RegExp(value); 32 | } 33 | 34 | // Handle Maps 35 | if (value instanceof Map) { 36 | const result = new Map(); 37 | value.forEach((val, key) => { 38 | result.set(key, deep_clone(val)); 39 | }); 40 | return result; 41 | } 42 | 43 | // Handle Sets 44 | if (value instanceof Set) { 45 | const result = new Set(); 46 | value.forEach((val) => { 47 | result.add(deep_clone(val)); 48 | }); 49 | return result; 50 | 51 | } 52 | 53 | // Handle Arrays 54 | if (Array.isArray(value)) { 55 | return value.map(deep_clone); 56 | } 57 | 58 | // Handle plain objects 59 | const result = Object.create(Object.getPrototypeOf(value)); 60 | for (const key in value) { 61 | if (value.hasOwnProperty(key)) { 62 | result[key] = deep_clone(value[key]); 63 | } 64 | } 65 | 66 | return result; 67 | } 68 | -------------------------------------------------------------------------------- /engine/src/utility/performance.js: -------------------------------------------------------------------------------- 1 | const performance_mark_names = new Map(); 2 | let trace_activated = false; 3 | 4 | /** 5 | * Sets the trace activation state. 6 | * @param {boolean} value - The new activation state. 7 | */ 8 | export function set_trace_activated(value) { 9 | trace_activated = value; 10 | } 11 | 12 | /** 13 | * Returns the current trace activation state. 14 | * @returns {boolean} The current activation state. 15 | */ 16 | export function is_trace_activated() { 17 | return trace_activated; 18 | } 19 | 20 | /** 21 | * Profiles a scope of code execution. Simply falls through to the callback if tracing is not activated. 22 | * @param {string} name - The name of the scope. 23 | * @param {function} fn - The function to profile. 24 | */ 25 | export function profile_scope(name, fn) { 26 | if (__DEV__ && trace_activated) { 27 | if (!performance_mark_names.has(name)) { 28 | performance_mark_names.set(name, { 29 | start: `${name}_start`, 30 | end: `${name}_end`, 31 | measure: `${name}`, 32 | }); 33 | } 34 | const mark_names = performance_mark_names.get(name); 35 | performance.mark(mark_names.start); 36 | } 37 | fn(); 38 | if (__DEV__ && trace_activated) { 39 | const mark_names = performance_mark_names.get(name); 40 | performance.mark(mark_names.end); 41 | performance.measure(mark_names.measure, mark_names.start, mark_names.end); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/favicon.ico -------------------------------------------------------------------------------- /forge.config.js: -------------------------------------------------------------------------------- 1 | const { FusesPlugin } = require('@electron-forge/plugin-fuses'); 2 | const { FuseV1Options, FuseVersion } = require('@electron/fuses'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | outDir: path.join(__dirname, 'executables'), 7 | packagerConfig: { 8 | asar: true, 9 | }, 10 | rebuildConfig: {}, 11 | makers: [ 12 | { 13 | name: '@electron-forge/maker-squirrel', 14 | config: { }, 15 | }, 16 | { 17 | name: '@electron-forge/maker-zip', 18 | platforms: ['darwin'], 19 | config: { } 20 | }, 21 | { 22 | name: '@electron-forge/maker-deb', 23 | config: { }, 24 | }, 25 | { 26 | name: '@electron-forge/maker-rpm', 27 | config: { }, 28 | }, 29 | ], 30 | plugins: [ 31 | { 32 | name: '@electron-forge/plugin-auto-unpack-natives', 33 | config: { }, 34 | }, 35 | // Fuses are used to enable/disable various Electron functionality 36 | // at package time, before code signing the application 37 | new FusesPlugin({ 38 | version: FuseVersion.V1, 39 | [FuseV1Options.RunAsNode]: false, 40 | [FuseV1Options.EnableCookieEncryption]: true, 41 | [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, 42 | [FuseV1Options.EnableNodeCliInspectArguments]: false, 43 | [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, 44 | [FuseV1Options.OnlyLoadAppFromAsar]: true, 45 | }), 46 | ], 47 | }; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Sundown 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sundown", 3 | "version": "1.0.0", 4 | "description": "A WebGPU game engine for fun and games.", 5 | "main": "dist/electron/main/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "cook": "node tools/fragment_preprocessor.js && node tools/msdf_font_generator.js", 10 | "predev": "npm run cook", 11 | "dev": "node tools/dev_server.js", 12 | "prebuild": "npm run cook", 13 | "build": "vite build", 14 | "predevtop": "npm run cook", 15 | "devtop": "vite build && electron-vite dev", 16 | "prebuildtop": "npm run cook", 17 | "buildtop": "vite build && electron-vite build", 18 | "premake": "npm run cook", 19 | "make": "vite build && electron-vite build && electron-forge make" 20 | }, 21 | "build": { 22 | "appId": "com.sunsetstudios.sundown", 23 | "mac": { 24 | "category": "simulation" 25 | }, 26 | "win": { 27 | "target": [ 28 | "nsis", 29 | "msi" 30 | ] 31 | }, 32 | "linux": { 33 | "target": [ 34 | "deb", 35 | "rpm", 36 | "AppImage" 37 | ] 38 | } 39 | }, 40 | "keywords": [ 41 | "WebGPU", 42 | "games", 43 | "game", 44 | "engine", 45 | "simulation", 46 | "renderer", 47 | "rendering" 48 | ], 49 | "author": "Adrian Sanchez", 50 | "license": "MIT", 51 | "dependencies": { 52 | "electron-squirrel-startup": "^1.0.1", 53 | "gl-matrix": "^3.4.3", 54 | "vite": "^6.3.5", 55 | "wgsl_reflect": "^1.0.8" 56 | }, 57 | "devDependencies": { 58 | "@electron-forge/cli": "^7.4.0", 59 | "@electron-forge/maker-deb": "^7.4.0", 60 | "@electron-forge/maker-rpm": "^7.4.0", 61 | "@electron-forge/maker-squirrel": "^7.4.0", 62 | "@electron-forge/maker-zip": "^7.4.0", 63 | "@electron-forge/plugin-auto-unpack-natives": "^7.4.0", 64 | "@electron-forge/plugin-fuses": "^7.4.0", 65 | "@electron/fuses": "^1.8.0", 66 | "electron": "^31.1.0", 67 | "electron-vite": "^3.1.0", 68 | "express": "^4.21.2", 69 | "msdf-bmfont-xml": "^2.5.4", 70 | "prettier": "^3.3.3" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); 2 | 3 | body, html { 4 | margin: 0; 5 | padding: 0; 6 | width: 100%; 7 | height: 100%; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | background-color: #000; 12 | overflow: hidden; 13 | font-family: 'Poppins', sans-serif; 14 | } 15 | 16 | canvas { 17 | position: absolute; 18 | width: 100%; 19 | height: 100%; 20 | background-color: #1e1e1e; 21 | } 22 | 23 | canvas#ui-canvas { 24 | background-color: #00000000; 25 | pointer-events: none; 26 | } 27 | -------------------------------------------------------------------------------- /sundown.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /sundown_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sunset-Studios/Sundown/137a3514199688776001b7228294e7b428edb8a9/sundown_demo.gif -------------------------------------------------------------------------------- /tools/dev_server.js: -------------------------------------------------------------------------------- 1 | import { createServer as createViteServer } from "vite"; 2 | import express from "express"; 3 | import fs from "fs/promises"; 4 | import path from "path"; 5 | import { fileURLToPath } from "url"; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | 10 | const CONFIG_BASE_PATH = path.resolve(__dirname, "../assets/config"); 11 | 12 | async function create_dev_server() { 13 | const app = express(); 14 | 15 | // Middleware to parse JSON bodies 16 | app.use(express.json()); 17 | 18 | // Create Vite server in middleware mode 19 | const vite = await createViteServer({ 20 | server: { middlewareMode: true }, 21 | appType: "custom", 22 | configFile: path.resolve(__dirname, "../vite.config.mjs"), 23 | }); 24 | 25 | app.use((req, res, next) => { 26 | res.setHeader("Cross-Origin-Opener-Policy", "same-origin"); 27 | res.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); 28 | next(); 29 | }); 30 | 31 | // Use Vite's connect instance as middleware 32 | app.use(vite.middlewares); 33 | 34 | // Serve general requests 35 | app.use("*", async (req, res, next) => { 36 | next(); 37 | }); 38 | 39 | // Serve index.html 40 | app.get("/", async (req, res, next) => { 41 | const url = req.originalUrl; 42 | 43 | try { 44 | let template = await fs.readFile(path.resolve(__dirname, "../index.html"), "utf-8"); 45 | 46 | template = await vite.transformIndexHtml(url, template); 47 | 48 | res.status(200).set({ "Content-Type": "text/html" }).end(template); 49 | } catch (e) { 50 | vite.ssrFixStacktrace(e); 51 | next(e); 52 | } 53 | }); 54 | 55 | // API endpoint to save config 56 | app.post("/sundown/dev/save-config", async (req, res) => { 57 | const { file_name, config } = req.body; 58 | if (!file_name || !config) { 59 | return res.status(400).json({ error: "Missing file_name or config in request body." }); 60 | } 61 | 62 | try { 63 | const file_path = path.join(CONFIG_BASE_PATH, `${file_name}.json`); 64 | await fs.writeFile(file_path, JSON.stringify(config, null, 2), "utf8"); 65 | res.json({ success: true }); 66 | } catch (err) { 67 | res.status(500).json({ error: err.message }); 68 | } 69 | }); 70 | 71 | // API endpoint to load config 72 | app.get("/sundown/dev/get-config", async (req, res) => { 73 | const { file_name } = req.query; 74 | if (!file_name) { 75 | return res.status(400).json({ error: "Missing file_name in query parameters." }); 76 | } 77 | 78 | try { 79 | const file_path = path.join(CONFIG_BASE_PATH, `${file_name}.json`); 80 | const data = await fs.readFile(file_path, "utf8"); 81 | const config = JSON.parse(data); 82 | res.json(config); 83 | } catch (err) { 84 | if (err.code === "ENOENT") { 85 | return res.status(404).json({ error: "Config file not found." }); 86 | } 87 | res.status(500).json({ error: err.message }); 88 | } 89 | }); 90 | 91 | const port = process.env.PORT || 3000; 92 | app.listen(port, () => { 93 | console.log( 94 | "\x1b[32mSundown Vite dev server running at \x1b[37mhttp://localhost:" + 95 | port + 96 | "\x1b[32m\x1b[0m" 97 | ); 98 | }); 99 | } 100 | 101 | create_dev_server().catch((err) => { 102 | console.error("Failed to start dev server:", err); 103 | process.exit(1); 104 | }); 105 | -------------------------------------------------------------------------------- /tools/msdf_font_generator.js: -------------------------------------------------------------------------------- 1 | import generateBMFont from "msdf-bmfont-xml"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | import { fileURLToPath } from "url"; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | 9 | const FONTS_BASE = `${__dirname}/../assets`; 10 | 11 | function find_font_files(dir, font_files) { 12 | const font_dir_files = fs.readdirSync(dir); 13 | for (const font_file of font_dir_files) { 14 | const full_path = path.join(dir, font_file); 15 | const stats = fs.statSync(full_path); 16 | 17 | if (stats.isDirectory()) { 18 | find_font_files(full_path, font_files); 19 | } else if (font_file.match(/\.(ttf|otf)$/i)) { 20 | font_files.push(full_path); 21 | } 22 | } 23 | } 24 | 25 | function find_fonts(dir, font_files) { 26 | const files = fs.readdirSync(dir); 27 | 28 | for (const file of files) { 29 | const full_path = path.join(dir, file); 30 | const stats = fs.statSync(full_path); 31 | 32 | if (stats.isDirectory()) { 33 | if (path.basename(full_path) === "fonts") { 34 | find_font_files(full_path, font_files); 35 | } 36 | find_fonts(full_path, font_files); 37 | } 38 | } 39 | } 40 | 41 | const font_files = []; 42 | find_fonts(FONTS_BASE, font_files); 43 | 44 | for (const font_file of font_files) { 45 | generateBMFont(font_file, { 46 | outputType: "json", 47 | }, (error, textures, font) => { 48 | if (error) throw error; 49 | 50 | // Write font textures 51 | textures.forEach((texture, index) => { 52 | const png_filename = texture.filename.endsWith(".png") 53 | ? texture.filename 54 | : texture.filename + ".png"; 55 | fs.writeFile(png_filename, texture.texture, (err) => { 56 | if (err) throw err; 57 | }); 58 | }); 59 | 60 | // Write font data file 61 | fs.writeFile(font.filename, font.data, (err) => { 62 | if (err) throw err; 63 | }); 64 | 65 | // Update font manifest 66 | const manifest_path = path.join(path.dirname(font.filename), 'font_manifest.json'); 67 | let manifest = { fonts: [] }; 68 | 69 | // Read existing manifest if it exists 70 | if (fs.existsSync(manifest_path)) { 71 | manifest = JSON.parse(fs.readFileSync(manifest_path)); 72 | } 73 | 74 | // Add/update font entry 75 | const relative_path = path.relative(FONTS_BASE, font.filename).split(path.sep).join('/'); 76 | const font_entry = { 77 | name: path.basename(relative_path, path.extname(relative_path)), 78 | path: relative_path 79 | }; 80 | 81 | // Check if font already exists in manifest 82 | const existing_index = manifest.fonts.findIndex(f => f.path === relative_path); 83 | if (existing_index >= 0) { 84 | manifest.fonts[existing_index] = font_entry; 85 | } else { 86 | manifest.fonts.push(font_entry); 87 | } 88 | 89 | // Write updated manifest 90 | fs.writeFileSync(manifest_path, JSON.stringify(manifest, null, 2)); 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | 3 | export default defineConfig({ 4 | base: "./", 5 | build: { 6 | outDir: "dist", 7 | rollupOptions: { 8 | input: { 9 | main: "index.html", 10 | }, 11 | }, 12 | }, 13 | publicDir: "assets", 14 | define: { 15 | __DEV__: "true", 16 | }, 17 | server: { 18 | // if you’re proxying through Express you may not need this; 19 | // otherwise Vite itself must serve these headers. 20 | headers: { 21 | "Cross-Origin-Opener-Policy": "same-origin", 22 | "Cross-Origin-Embedder-Policy": "require-corp", 23 | }, 24 | }, 25 | }); 26 | --------------------------------------------------------------------------------