From dc3b82de5e0b6f9cdce19b756298395c0359ed53 Mon Sep 17 00:00:00 2001 From: ouczb Date: Sun, 14 Jan 2024 22:56:06 +0800 Subject: [PATCH] engine upload --- .gitignore | 3 + assets/assets.go | 53 ++ assets/fonts/MaterialIcons-Regular.ttf | Bin 0 -> 356840 bytes assets/fonts/SourceCodeProRegular.ttf | Bin 0 -> 197644 bytes assets/fonts/SourceSansPro-Bold.ttf | Bin 0 -> 247412 bytes assets/fonts/SourceSansPro-Italic.ttf | Bin 0 -> 109456 bytes assets/fonts/SourceSansPro-Regular.ttf | Bin 0 -> 248132 bytes assets/models/sphere.glb | Bin 0 -> 86192 bytes assets/shaders/blur.fs.glsl | 24 + assets/shaders/blur.json | 15 + assets/shaders/blur.vs.glsl | 19 + assets/shaders/deferred/color.fs.glsl | 14 + assets/shaders/deferred/color.json | 22 + assets/shaders/deferred/color.vs.glsl | 31 + assets/shaders/deferred/textured.fs.glsl | 18 + assets/shaders/deferred/textured.json | 23 + assets/shaders/deferred/textured.vs.glsl | 31 + assets/shaders/depth.fs.glsl | 15 + assets/shaders/depth.json | 18 + assets/shaders/depth.vs.glsl | 35 ++ assets/shaders/forward/color.fs.glsl | 24 + assets/shaders/forward/color.json | 22 + assets/shaders/forward/color.vs.glsl | 33 ++ assets/shaders/forward/skybox.fs.glsl | 37 ++ assets/shaders/forward/skybox.json | 22 + assets/shaders/forward/skybox.vs.glsl | 33 ++ assets/shaders/forward/sprite.fs.glsl | 24 + assets/shaders/forward/sprite.json | 23 + assets/shaders/forward/sprite.vs.glsl | 42 ++ assets/shaders/forward/textured.fs.glsl | 28 + assets/shaders/forward/textured.json | 23 + assets/shaders/forward/textured.vs.glsl | 33 ++ assets/shaders/game/voxels.fs.glsl | 12 + assets/shaders/game/voxels.json | 26 + assets/shaders/game/voxels.vs.glsl | 43 ++ assets/shaders/lib/common.glsl | 44 ++ assets/shaders/lib/deferred_fragment.glsl | 10 + assets/shaders/lib/deferred_vertex.glsl | 11 + assets/shaders/lib/forward_fragment.glsl | 8 + assets/shaders/lib/forward_vertex.glsl | 10 + assets/shaders/lib/lighting.glsl | 214 +++++++ assets/shaders/lib/ui.glsl | 23 + assets/shaders/light.fs.glsl | 60 ++ assets/shaders/light.json | 21 + assets/shaders/light.vs.glsl | 19 + assets/shaders/lines.fs.glsl | 20 + assets/shaders/lines.json | 18 + assets/shaders/lines.vs.glsl | 24 + assets/shaders/old/billboard.fs.glsl | 11 + assets/shaders/old/billboard.gs.glsl | 43 ++ assets/shaders/old/billboard.vs.glsl | 10 + assets/shaders/output.fs.glsl | 13 + assets/shaders/output.json | 15 + assets/shaders/output.vs.glsl | 19 + assets/shaders/postprocess.fs.glsl | 54 ++ assets/shaders/postprocess.json | 16 + assets/shaders/postprocess.vs.glsl | 19 + assets/shaders/shadow.fs.glsl | 13 + assets/shaders/shadow.json | 14 + assets/shaders/shadow.vs.glsl | 25 + assets/shaders/ssao.fs.glsl | 73 +++ assets/shaders/ssao.json | 18 + assets/shaders/ssao.vs.glsl | 19 + assets/shaders/ui_quad.fs.glsl | 56 ++ assets/shaders/ui_quad.json | 13 + assets/shaders/ui_quad.vs.glsl | 47 ++ assets/textures/color_grading/none.png | Bin 0 -> 646 bytes assets/textures/fire.png | Bin 0 -> 3642 bytes assets/textures/heightmap.png | Bin 0 -> 37173 bytes assets/textures/palette.png | Bin 0 -> 2096 bytes assets/textures/shit_logo.png | Bin 0 -> 6629 bytes assets/textures/ui/light.png | Bin 0 -> 2933 bytes assets/textures/uv_checker.png | Bin 0 -> 28810 bytes engine/frame.go | 45 ++ engine/frame_counter.go | 76 +++ engine/interrupter.go | 39 ++ engine/object/actor.go | 228 ++++++++ engine/object/builder.go | 99 ++++ engine/object/camera/camera.go | 102 ++++ engine/object/camera/frustum.go | 57 ++ engine/object/component.go | 141 +++++ engine/object/light/directional.go | 236 ++++++++ engine/object/light/light.go | 37 ++ engine/object/light/point.go | 89 +++ engine/object/light/type.go | 12 + engine/object/mesh/dynamic.go | 67 +++ engine/object/mesh/mesh.go | 149 +++++ engine/object/property.go | 100 ++++ engine/object/query.go | 119 ++++ engine/object/relations.go | 300 ++++++++++ engine/object/serialize.go | 195 ++++++ engine/object/type.go | 11 + engine/object/utils.go | 19 + engine/profiling.go | 16 + engine/render/graph/default.go | 98 ++++ engine/render/graph/graph.go | 174 ++++++ engine/render/graph/node.go | 185 ++++++ engine/render/graph/post_node.go | 44 ++ engine/render/graph/pre_node.go | 100 ++++ engine/render/pass/basic_material.go | 66 +++ engine/render/pass/blur.go | 127 ++++ engine/render/pass/deferred_cache.go | 95 +++ engine/render/pass/deferred_geometry.go | 149 +++++ engine/render/pass/deferred_lighting.go | 146 +++++ engine/render/pass/deferred_material.go | 70 +++ engine/render/pass/depth_cache.go | 88 +++ engine/render/pass/depth_pass.go | 114 ++++ engine/render/pass/depth_sorter.go | 26 + engine/render/pass/draw_group.go | 88 +++ engine/render/pass/effect/particle_pass.go | 133 +++++ engine/render/pass/forward.go | 131 +++++ engine/render/pass/forward_cache.go | 104 ++++ engine/render/pass/forward_material.go | 85 +++ engine/render/pass/gbuffer.go | 94 +++ engine/render/pass/light_buffer.go | 50 ++ engine/render/pass/light_shader.go | 152 +++++ engine/render/pass/line_cache.go | 88 +++ engine/render/pass/lines.go | 111 ++++ engine/render/pass/material.go | 68 +++ engine/render/pass/object_buffer.go | 30 + engine/render/pass/output.go | 130 ++++ engine/render/pass/pass.go | 16 + engine/render/pass/postprocess.go | 143 +++++ engine/render/pass/shadow.go | 206 +++++++ engine/render/pass/shadow_cache.go | 36 ++ engine/render/pass/shadow_mat_cache.go | 88 +++ engine/render/pass/ssao.go | 267 +++++++++ engine/render/uniform/camera.go | 19 + engine/render/uniform/light.go | 41 ++ engine/render/uniform/object.go | 8 + engine/renderapi/args.go | 58 ++ engine/renderapi/buffer/array.go | 73 +++ engine/renderapi/buffer/buffer.go | 125 ++++ engine/renderapi/buffer/item.go | 47 ++ engine/renderapi/buffer/util.go | 18 + engine/renderapi/cache/allocator/allocator.go | 177 ++++++ .../cache/allocator/allocator_test.go | 49 ++ .../renderapi/cache/allocator/suite_test.go | 13 + engine/renderapi/cache/cache.go | 210 +++++++ engine/renderapi/cache/mesh.go | 50 ++ engine/renderapi/cache/mesh_cache.go | 91 +++ engine/renderapi/cache/sampler_cache.go | 158 +++++ engine/renderapi/cache/shader_cache.go | 37 ++ engine/renderapi/cache/texture_cache.go | 86 +++ engine/renderapi/color/color.go | 181 ++++++ engine/renderapi/color/color_suite_test.go | 41 ++ engine/renderapi/color/palette.go | 28 + engine/renderapi/command/buffer.go | 329 +++++++++++ engine/renderapi/command/pool.go | 62 ++ engine/renderapi/command/recorder.go | 26 + engine/renderapi/command/thread_worker.go | 70 +++ engine/renderapi/command/worker.go | 153 +++++ engine/renderapi/descriptor/descriptor.go | 21 + .../renderapi/descriptor/descriptor_struct.go | 115 ++++ .../descriptor/descriptor_struct_test.go | 45 ++ .../descriptor/descriptor_suite_test.go | 13 + .../renderapi/descriptor/input_attachment.go | 71 +++ engine/renderapi/descriptor/layout.go | 138 +++++ engine/renderapi/descriptor/pool.go | 115 ++++ engine/renderapi/descriptor/sampler.go | 69 +++ engine/renderapi/descriptor/sampler_array.go | 125 ++++ engine/renderapi/descriptor/set.go | 29 + engine/renderapi/descriptor/set_mock.go | 17 + engine/renderapi/descriptor/storage.go | 92 +++ engine/renderapi/descriptor/uniform.go | 80 +++ engine/renderapi/descriptor/uniform_array.go | 88 +++ engine/renderapi/device/device.go | 174 ++++++ engine/renderapi/device/device_help.go | 52 ++ engine/renderapi/device/memcpy.go | 8 + engine/renderapi/device/memory.go | 211 +++++++ engine/renderapi/font/font.go | 180 ++++++ engine/renderapi/font/font_suite_test.go | 33 ++ engine/renderapi/font/glyph.go | 34 ++ engine/renderapi/font/loader.go | 75 +++ engine/renderapi/framebuffer/array.go | 29 + engine/renderapi/framebuffer/framebuffer.go | 152 +++++ engine/renderapi/image/format.go | 5 + engine/renderapi/image/image.go | 209 +++++++ engine/renderapi/image/loader.go | 40 ++ engine/renderapi/image/view.go | 34 ++ engine/renderapi/material/def.go | 65 ++ engine/renderapi/material/instance.go | 20 + engine/renderapi/material/material.go | 128 ++++ engine/renderapi/material/types.go | 76 +++ engine/renderapi/noise/white_noise.go | 48 ++ engine/renderapi/pipeline/args.go | 50 ++ engine/renderapi/pipeline/layout.go | 63 ++ engine/renderapi/pipeline/pipeline.go | 299 ++++++++++ engine/renderapi/renderpass/args.go | 14 + .../renderpass/attachment/attachment.go | 44 ++ .../renderapi/renderpass/attachment/blend.go | 47 ++ .../renderpass/attachment/color_attachment.go | 54 ++ .../renderpass/attachment/depth_attachment.go | 58 ++ .../renderapi/renderpass/attachment/image.go | 105 ++++ engine/renderapi/renderpass/renderpass.go | 200 +++++++ engine/renderapi/renderpass/subpass.go | 35 ++ engine/renderapi/shader/details.go | 50 ++ engine/renderapi/shader/module.go | 68 +++ engine/renderapi/shader/ref.go | 30 + engine/renderapi/shader/shader.go | 101 ++++ engine/renderapi/shader/stage.go | 21 + engine/renderapi/shader/util.go | 87 +++ engine/renderapi/swapchain/context.go | 54 ++ engine/renderapi/swapchain/swapchain.go | 208 +++++++ engine/renderapi/sync/fence.go | 77 +++ engine/renderapi/sync/mutex.go | 5 + engine/renderapi/sync/semaphore.go | 58 ++ engine/renderapi/texture/const.go | 14 + engine/renderapi/texture/ref.go | 60 ++ engine/renderapi/texture/slot.go | 6 + engine/renderapi/texture/texture.go | 140 +++++ engine/renderapi/types/types.go | 159 +++++ engine/renderapi/upload/texture.go | 173 ++++++ engine/renderapi/vertex/cull_mode.go | 19 + engine/renderapi/vertex/format.go | 63 ++ engine/renderapi/vertex/index_type.go | 14 + engine/renderapi/vertex/mesh.go | 135 +++++ engine/renderapi/vertex/mesh_generated.go | 28 + engine/renderapi/vertex/optimize.go | 27 + engine/renderapi/vertex/pointer.go | 21 + engine/renderapi/vertex/pointers.go | 21 + engine/renderapi/vertex/primitives.go | 11 + engine/renderapi/vertex/quad.go | 19 + engine/renderapi/vertex/tag.go | 94 +++ engine/renderapi/vertex/triangle.go | 23 + engine/renderapi/vertex/vertex_suite_test.go | 39 ++ engine/renderapi/vkerror/errors.go | 28 + engine/renderapi/vulkan/backend.go | 131 +++++ engine/renderapi/vulkan/init.go | 17 + engine/renderapi/vulkan/instance/instance.go | 61 ++ .../vulkan/instance/instance_help.go | 43 ++ engine/renderapi/vulkan/pool.go | 22 + engine/renderapi/vulkan/target.go | 110 ++++ engine/renderapi/vulkan/util.go | 30 + engine/renderapi/vulkan/window.go | 185 ++++++ engine/run.go | 81 +++ engine/util/align.go | 35 ++ engine/util/align_test.go | 46 ++ engine/util/map.go | 21 + engine/util/slice.go | 68 +++ engine/util/strings.go | 16 + engine/util/sync_map.go | 30 + engine/util/timer.go | 21 + engine/util/util_suite_test.go | 13 + engine/util/uuid.go | 17 + game/main.go | 13 + go.mod | 32 + go.sum | 78 +++ plugins/geometry/cone/cone.go | 91 +++ plugins/geometry/cube/cube.go | 118 ++++ plugins/geometry/cylinder/cylinder.go | 105 ++++ plugins/geometry/gltf/asset.go | 204 +++++++ plugins/geometry/gltf/mesh.go | 37 ++ plugins/geometry/lines/box.go | 91 +++ plugins/geometry/lines/immediate.go | 50 ++ plugins/geometry/lines/line.go | 17 + plugins/geometry/lines/lines.go | 62 ++ plugins/geometry/lines/sphere.go | 101 ++++ plugins/geometry/lines/wireframe.go | 53 ++ plugins/geometry/plane/plane.go | 73 +++ plugins/geometry/sphere/sphere.go | 115 ++++ plugins/geometry/sprite/material.go | 21 + plugins/geometry/sprite/sprite.go | 71 +++ plugins/math/byte4/byte4.go | 10 + plugins/math/ivec2/ivec2.go | 32 + plugins/math/mat4/mat4.go | 317 ++++++++++ plugins/math/mat4/operations.go | 11 + plugins/math/mat4/project.go | 67 +++ plugins/math/mat4/project_test.go | 56 ++ plugins/math/mat4/translation.go | 20 + plugins/math/math32.go | 177 ++++++ plugins/math/noise.go | 28 + plugins/math/quat/quat.go | 553 ++++++++++++++++++ plugins/math/quat/quat_suite_test.go | 28 + plugins/math/random/random.go | 28 + plugins/math/shape/frustum.go | 107 ++++ plugins/math/shape/sphere.go | 15 + plugins/math/transform/transform.go | 273 +++++++++ plugins/math/vec2/array.go | 21 + plugins/math/vec2/operations.go | 37 ++ plugins/math/vec2/vec2.go | 134 +++++ plugins/math/vec3/array.go | 21 + plugins/math/vec3/operations.go | 85 +++ plugins/math/vec3/vec3.go | 205 +++++++ plugins/math/vec4/array.go | 21 + plugins/math/vec4/operations.go | 56 ++ plugins/math/vec4/vec4.go | 144 +++++ plugins/system/events/event.go | 30 + plugins/system/input/debug.go | 39 ++ plugins/system/input/handler.go | 19 + plugins/system/input/keys/action.go | 28 + plugins/system/input/keys/event.go | 64 ++ plugins/system/input/keys/handler.go | 70 +++ plugins/system/input/keys/keycodes.go | 68 +++ plugins/system/input/keys/modifier.go | 13 + plugins/system/input/keys/statemap.go | 55 ++ plugins/system/input/keys/util.go | 35 ++ plugins/system/input/mouse/action.go | 28 + plugins/system/input/mouse/button.go | 21 + plugins/system/input/mouse/event.go | 92 +++ plugins/system/input/mouse/handler.go | 73 +++ plugins/system/input/mouse/statemap.go | 34 ++ plugins/system/input/mouse/utils.go | 25 + 303 files changed, 20327 insertions(+) create mode 100644 .gitignore create mode 100644 assets/assets.go create mode 100644 assets/fonts/MaterialIcons-Regular.ttf create mode 100644 assets/fonts/SourceCodeProRegular.ttf create mode 100644 assets/fonts/SourceSansPro-Bold.ttf create mode 100644 assets/fonts/SourceSansPro-Italic.ttf create mode 100644 assets/fonts/SourceSansPro-Regular.ttf create mode 100644 assets/models/sphere.glb create mode 100644 assets/shaders/blur.fs.glsl create mode 100644 assets/shaders/blur.json create mode 100644 assets/shaders/blur.vs.glsl create mode 100644 assets/shaders/deferred/color.fs.glsl create mode 100644 assets/shaders/deferred/color.json create mode 100644 assets/shaders/deferred/color.vs.glsl create mode 100644 assets/shaders/deferred/textured.fs.glsl create mode 100644 assets/shaders/deferred/textured.json create mode 100644 assets/shaders/deferred/textured.vs.glsl create mode 100644 assets/shaders/depth.fs.glsl create mode 100644 assets/shaders/depth.json create mode 100644 assets/shaders/depth.vs.glsl create mode 100644 assets/shaders/forward/color.fs.glsl create mode 100644 assets/shaders/forward/color.json create mode 100644 assets/shaders/forward/color.vs.glsl create mode 100644 assets/shaders/forward/skybox.fs.glsl create mode 100644 assets/shaders/forward/skybox.json create mode 100644 assets/shaders/forward/skybox.vs.glsl create mode 100644 assets/shaders/forward/sprite.fs.glsl create mode 100644 assets/shaders/forward/sprite.json create mode 100644 assets/shaders/forward/sprite.vs.glsl create mode 100644 assets/shaders/forward/textured.fs.glsl create mode 100644 assets/shaders/forward/textured.json create mode 100644 assets/shaders/forward/textured.vs.glsl create mode 100644 assets/shaders/game/voxels.fs.glsl create mode 100644 assets/shaders/game/voxels.json create mode 100644 assets/shaders/game/voxels.vs.glsl create mode 100644 assets/shaders/lib/common.glsl create mode 100644 assets/shaders/lib/deferred_fragment.glsl create mode 100644 assets/shaders/lib/deferred_vertex.glsl create mode 100644 assets/shaders/lib/forward_fragment.glsl create mode 100644 assets/shaders/lib/forward_vertex.glsl create mode 100644 assets/shaders/lib/lighting.glsl create mode 100644 assets/shaders/lib/ui.glsl create mode 100644 assets/shaders/light.fs.glsl create mode 100644 assets/shaders/light.json create mode 100644 assets/shaders/light.vs.glsl create mode 100644 assets/shaders/lines.fs.glsl create mode 100644 assets/shaders/lines.json create mode 100644 assets/shaders/lines.vs.glsl create mode 100644 assets/shaders/old/billboard.fs.glsl create mode 100644 assets/shaders/old/billboard.gs.glsl create mode 100644 assets/shaders/old/billboard.vs.glsl create mode 100644 assets/shaders/output.fs.glsl create mode 100644 assets/shaders/output.json create mode 100644 assets/shaders/output.vs.glsl create mode 100644 assets/shaders/postprocess.fs.glsl create mode 100644 assets/shaders/postprocess.json create mode 100644 assets/shaders/postprocess.vs.glsl create mode 100644 assets/shaders/shadow.fs.glsl create mode 100644 assets/shaders/shadow.json create mode 100644 assets/shaders/shadow.vs.glsl create mode 100644 assets/shaders/ssao.fs.glsl create mode 100644 assets/shaders/ssao.json create mode 100644 assets/shaders/ssao.vs.glsl create mode 100644 assets/shaders/ui_quad.fs.glsl create mode 100644 assets/shaders/ui_quad.json create mode 100644 assets/shaders/ui_quad.vs.glsl create mode 100644 assets/textures/color_grading/none.png create mode 100644 assets/textures/fire.png create mode 100644 assets/textures/heightmap.png create mode 100644 assets/textures/palette.png create mode 100644 assets/textures/shit_logo.png create mode 100644 assets/textures/ui/light.png create mode 100644 assets/textures/uv_checker.png create mode 100644 engine/frame.go create mode 100644 engine/frame_counter.go create mode 100644 engine/interrupter.go create mode 100644 engine/object/actor.go create mode 100644 engine/object/builder.go create mode 100644 engine/object/camera/camera.go create mode 100644 engine/object/camera/frustum.go create mode 100644 engine/object/component.go create mode 100644 engine/object/light/directional.go create mode 100644 engine/object/light/light.go create mode 100644 engine/object/light/point.go create mode 100644 engine/object/light/type.go create mode 100644 engine/object/mesh/dynamic.go create mode 100644 engine/object/mesh/mesh.go create mode 100644 engine/object/property.go create mode 100644 engine/object/query.go create mode 100644 engine/object/relations.go create mode 100644 engine/object/serialize.go create mode 100644 engine/object/type.go create mode 100644 engine/object/utils.go create mode 100644 engine/profiling.go create mode 100644 engine/render/graph/default.go create mode 100644 engine/render/graph/graph.go create mode 100644 engine/render/graph/node.go create mode 100644 engine/render/graph/post_node.go create mode 100644 engine/render/graph/pre_node.go create mode 100644 engine/render/pass/basic_material.go create mode 100644 engine/render/pass/blur.go create mode 100644 engine/render/pass/deferred_cache.go create mode 100644 engine/render/pass/deferred_geometry.go create mode 100644 engine/render/pass/deferred_lighting.go create mode 100644 engine/render/pass/deferred_material.go create mode 100644 engine/render/pass/depth_cache.go create mode 100644 engine/render/pass/depth_pass.go create mode 100644 engine/render/pass/depth_sorter.go create mode 100644 engine/render/pass/draw_group.go create mode 100644 engine/render/pass/effect/particle_pass.go create mode 100644 engine/render/pass/forward.go create mode 100644 engine/render/pass/forward_cache.go create mode 100644 engine/render/pass/forward_material.go create mode 100644 engine/render/pass/gbuffer.go create mode 100644 engine/render/pass/light_buffer.go create mode 100644 engine/render/pass/light_shader.go create mode 100644 engine/render/pass/line_cache.go create mode 100644 engine/render/pass/lines.go create mode 100644 engine/render/pass/material.go create mode 100644 engine/render/pass/object_buffer.go create mode 100644 engine/render/pass/output.go create mode 100644 engine/render/pass/pass.go create mode 100644 engine/render/pass/postprocess.go create mode 100644 engine/render/pass/shadow.go create mode 100644 engine/render/pass/shadow_cache.go create mode 100644 engine/render/pass/shadow_mat_cache.go create mode 100644 engine/render/pass/ssao.go create mode 100644 engine/render/uniform/camera.go create mode 100644 engine/render/uniform/light.go create mode 100644 engine/render/uniform/object.go create mode 100644 engine/renderapi/args.go create mode 100644 engine/renderapi/buffer/array.go create mode 100644 engine/renderapi/buffer/buffer.go create mode 100644 engine/renderapi/buffer/item.go create mode 100644 engine/renderapi/buffer/util.go create mode 100644 engine/renderapi/cache/allocator/allocator.go create mode 100644 engine/renderapi/cache/allocator/allocator_test.go create mode 100644 engine/renderapi/cache/allocator/suite_test.go create mode 100644 engine/renderapi/cache/cache.go create mode 100644 engine/renderapi/cache/mesh.go create mode 100644 engine/renderapi/cache/mesh_cache.go create mode 100644 engine/renderapi/cache/sampler_cache.go create mode 100644 engine/renderapi/cache/shader_cache.go create mode 100644 engine/renderapi/cache/texture_cache.go create mode 100644 engine/renderapi/color/color.go create mode 100644 engine/renderapi/color/color_suite_test.go create mode 100644 engine/renderapi/color/palette.go create mode 100644 engine/renderapi/command/buffer.go create mode 100644 engine/renderapi/command/pool.go create mode 100644 engine/renderapi/command/recorder.go create mode 100644 engine/renderapi/command/thread_worker.go create mode 100644 engine/renderapi/command/worker.go create mode 100644 engine/renderapi/descriptor/descriptor.go create mode 100644 engine/renderapi/descriptor/descriptor_struct.go create mode 100644 engine/renderapi/descriptor/descriptor_struct_test.go create mode 100644 engine/renderapi/descriptor/descriptor_suite_test.go create mode 100644 engine/renderapi/descriptor/input_attachment.go create mode 100644 engine/renderapi/descriptor/layout.go create mode 100644 engine/renderapi/descriptor/pool.go create mode 100644 engine/renderapi/descriptor/sampler.go create mode 100644 engine/renderapi/descriptor/sampler_array.go create mode 100644 engine/renderapi/descriptor/set.go create mode 100644 engine/renderapi/descriptor/set_mock.go create mode 100644 engine/renderapi/descriptor/storage.go create mode 100644 engine/renderapi/descriptor/uniform.go create mode 100644 engine/renderapi/descriptor/uniform_array.go create mode 100644 engine/renderapi/device/device.go create mode 100644 engine/renderapi/device/device_help.go create mode 100644 engine/renderapi/device/memcpy.go create mode 100644 engine/renderapi/device/memory.go create mode 100644 engine/renderapi/font/font.go create mode 100644 engine/renderapi/font/font_suite_test.go create mode 100644 engine/renderapi/font/glyph.go create mode 100644 engine/renderapi/font/loader.go create mode 100644 engine/renderapi/framebuffer/array.go create mode 100644 engine/renderapi/framebuffer/framebuffer.go create mode 100644 engine/renderapi/image/format.go create mode 100644 engine/renderapi/image/image.go create mode 100644 engine/renderapi/image/loader.go create mode 100644 engine/renderapi/image/view.go create mode 100644 engine/renderapi/material/def.go create mode 100644 engine/renderapi/material/instance.go create mode 100644 engine/renderapi/material/material.go create mode 100644 engine/renderapi/material/types.go create mode 100644 engine/renderapi/noise/white_noise.go create mode 100644 engine/renderapi/pipeline/args.go create mode 100644 engine/renderapi/pipeline/layout.go create mode 100644 engine/renderapi/pipeline/pipeline.go create mode 100644 engine/renderapi/renderpass/args.go create mode 100644 engine/renderapi/renderpass/attachment/attachment.go create mode 100644 engine/renderapi/renderpass/attachment/blend.go create mode 100644 engine/renderapi/renderpass/attachment/color_attachment.go create mode 100644 engine/renderapi/renderpass/attachment/depth_attachment.go create mode 100644 engine/renderapi/renderpass/attachment/image.go create mode 100644 engine/renderapi/renderpass/renderpass.go create mode 100644 engine/renderapi/renderpass/subpass.go create mode 100644 engine/renderapi/shader/details.go create mode 100644 engine/renderapi/shader/module.go create mode 100644 engine/renderapi/shader/ref.go create mode 100644 engine/renderapi/shader/shader.go create mode 100644 engine/renderapi/shader/stage.go create mode 100644 engine/renderapi/shader/util.go create mode 100644 engine/renderapi/swapchain/context.go create mode 100644 engine/renderapi/swapchain/swapchain.go create mode 100644 engine/renderapi/sync/fence.go create mode 100644 engine/renderapi/sync/mutex.go create mode 100644 engine/renderapi/sync/semaphore.go create mode 100644 engine/renderapi/texture/const.go create mode 100644 engine/renderapi/texture/ref.go create mode 100644 engine/renderapi/texture/slot.go create mode 100644 engine/renderapi/texture/texture.go create mode 100644 engine/renderapi/types/types.go create mode 100644 engine/renderapi/upload/texture.go create mode 100644 engine/renderapi/vertex/cull_mode.go create mode 100644 engine/renderapi/vertex/format.go create mode 100644 engine/renderapi/vertex/index_type.go create mode 100644 engine/renderapi/vertex/mesh.go create mode 100644 engine/renderapi/vertex/mesh_generated.go create mode 100644 engine/renderapi/vertex/optimize.go create mode 100644 engine/renderapi/vertex/pointer.go create mode 100644 engine/renderapi/vertex/pointers.go create mode 100644 engine/renderapi/vertex/primitives.go create mode 100644 engine/renderapi/vertex/quad.go create mode 100644 engine/renderapi/vertex/tag.go create mode 100644 engine/renderapi/vertex/triangle.go create mode 100644 engine/renderapi/vertex/vertex_suite_test.go create mode 100644 engine/renderapi/vkerror/errors.go create mode 100644 engine/renderapi/vulkan/backend.go create mode 100644 engine/renderapi/vulkan/init.go create mode 100644 engine/renderapi/vulkan/instance/instance.go create mode 100644 engine/renderapi/vulkan/instance/instance_help.go create mode 100644 engine/renderapi/vulkan/pool.go create mode 100644 engine/renderapi/vulkan/target.go create mode 100644 engine/renderapi/vulkan/util.go create mode 100644 engine/renderapi/vulkan/window.go create mode 100644 engine/run.go create mode 100644 engine/util/align.go create mode 100644 engine/util/align_test.go create mode 100644 engine/util/map.go create mode 100644 engine/util/slice.go create mode 100644 engine/util/strings.go create mode 100644 engine/util/sync_map.go create mode 100644 engine/util/timer.go create mode 100644 engine/util/util_suite_test.go create mode 100644 engine/util/uuid.go create mode 100644 game/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 plugins/geometry/cone/cone.go create mode 100644 plugins/geometry/cube/cube.go create mode 100644 plugins/geometry/cylinder/cylinder.go create mode 100644 plugins/geometry/gltf/asset.go create mode 100644 plugins/geometry/gltf/mesh.go create mode 100644 plugins/geometry/lines/box.go create mode 100644 plugins/geometry/lines/immediate.go create mode 100644 plugins/geometry/lines/line.go create mode 100644 plugins/geometry/lines/lines.go create mode 100644 plugins/geometry/lines/sphere.go create mode 100644 plugins/geometry/lines/wireframe.go create mode 100644 plugins/geometry/plane/plane.go create mode 100644 plugins/geometry/sphere/sphere.go create mode 100644 plugins/geometry/sprite/material.go create mode 100644 plugins/geometry/sprite/sprite.go create mode 100644 plugins/math/byte4/byte4.go create mode 100644 plugins/math/ivec2/ivec2.go create mode 100644 plugins/math/mat4/mat4.go create mode 100644 plugins/math/mat4/operations.go create mode 100644 plugins/math/mat4/project.go create mode 100644 plugins/math/mat4/project_test.go create mode 100644 plugins/math/mat4/translation.go create mode 100644 plugins/math/math32.go create mode 100644 plugins/math/noise.go create mode 100644 plugins/math/quat/quat.go create mode 100644 plugins/math/quat/quat_suite_test.go create mode 100644 plugins/math/random/random.go create mode 100644 plugins/math/shape/frustum.go create mode 100644 plugins/math/shape/sphere.go create mode 100644 plugins/math/transform/transform.go create mode 100644 plugins/math/vec2/array.go create mode 100644 plugins/math/vec2/operations.go create mode 100644 plugins/math/vec2/vec2.go create mode 100644 plugins/math/vec3/array.go create mode 100644 plugins/math/vec3/operations.go create mode 100644 plugins/math/vec3/vec3.go create mode 100644 plugins/math/vec4/array.go create mode 100644 plugins/math/vec4/operations.go create mode 100644 plugins/math/vec4/vec4.go create mode 100644 plugins/system/events/event.go create mode 100644 plugins/system/input/debug.go create mode 100644 plugins/system/input/handler.go create mode 100644 plugins/system/input/keys/action.go create mode 100644 plugins/system/input/keys/event.go create mode 100644 plugins/system/input/keys/handler.go create mode 100644 plugins/system/input/keys/keycodes.go create mode 100644 plugins/system/input/keys/modifier.go create mode 100644 plugins/system/input/keys/statemap.go create mode 100644 plugins/system/input/keys/util.go create mode 100644 plugins/system/input/mouse/action.go create mode 100644 plugins/system/input/mouse/button.go create mode 100644 plugins/system/input/mouse/event.go create mode 100644 plugins/system/input/mouse/handler.go create mode 100644 plugins/system/input/mouse/statemap.go create mode 100644 plugins/system/input/mouse/utils.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b65fdd --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.log +.idea/* +*/.ipynb_checkpoints/* \ No newline at end of file diff --git a/assets/assets.go b/assets/assets.go new file mode 100644 index 0000000..2b26101 --- /dev/null +++ b/assets/assets.go @@ -0,0 +1,53 @@ +package assets + +import ( + "fmt" + "io" + "io/fs" + "os" + "path/filepath" +) + +var vfs fs.FS +var Path string + +func init() { + cwd, err := os.Getwd() + if err != nil { + panic(err) + } + Path = FindFileInParents("assets", cwd) + vfs = os.DirFS(Path) +} + +func Open(fileName string) (fs.File, error) { + return vfs.Open(fileName) +} + +func ReadAll(fileName string) ([]byte, error) { + file, err := Open(fileName) + if err != nil { + return nil, fmt.Errorf("error opening file %s: %w", fileName, err) + } + defer file.Close() + + data, err := io.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("error reading file %s: %w", fileName, err) + } + + return data, nil +} + +func FindFileInParents(name, path string) string { + files, err := os.ReadDir(path) + if err != nil { + panic(err) + } + for _, file := range files { + if file.Name() == name { + return filepath.Join(path, name) + } + } + return FindFileInParents(name, filepath.Dir(path)) +} diff --git a/assets/fonts/MaterialIcons-Regular.ttf b/assets/fonts/MaterialIcons-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..9d09b0feb85c35beeaddd31246be0b7c8e0e69a4 GIT binary patch literal 356840 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

t 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/SourceCodeProRegular.ttf b/assets/fonts/SourceCodeProRegular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..437f47280b4f611f2b211506c5c5eb887661194f GIT binary patch literal 197644 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%0A_ga!fgk_H{|QsoYlXD8DIo)mg2t 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/SourceSansPro-Regular.ttf b/assets/fonts/SourceSansPro-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..98e8579745c9afe420cb50d573ff4e93c93929a6 GIT binary patch literal 248132 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 literal 0 HcmV?d00001 diff --git a/assets/models/sphere.glb b/assets/models/sphere.glb new file mode 100644 index 0000000000000000000000000000000000000000..fb6623e7cb619577ba77e5e54c300d6c428edd68 GIT binary patch literal 86192 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 depth texture space +const mat4 biasMat = mat4( + 0.5, 0.0, 0.0, 0.0, + 0.0, 0.5, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.5, 0.5, 0.0, 1.0 +); + +// +// Lighting functions +// + +float _shadow_texture(uint index, vec2 point); +vec2 _shadow_size(uint index); +float sampleShadowmap(uint shadowmap, mat4 viewProj, vec3 position, float bias); +float sampleShadowmapPCF(uint shadowmap, mat4 viewProj, vec3 position, LightSettings settings); +float blendCascades(Light light, vec3 position, float depth, float blendRange, LightSettings settings); +float calculatePointLightContrib(Light light, vec3 surfaceToLight, float distanceToLight, vec3 normal); +vec3 ambientLight(LightSettings settings, float occlusion); +vec3 calculateLightColor(Light light, vec3 position, vec3 normal, float depth, LightSettings settings); + +float sampleShadowmap(uint shadowmap, mat4 viewProj, vec3 position, float bias) { + vec4 shadowCoord = biasMat * viewProj * vec4(position, 1); + + float shadow = 1.0; + if (shadowCoord.z > -1.0 && shadowCoord.z < 1.0 && shadowCoord.w > 0) { + float dist = _shadow_texture(shadowmap, shadowCoord.st); + float actual = exp(SHADOW_POWER * shadowCoord.z - bias) / exp(SHADOW_POWER); + + if (dist < actual) { + shadow = 0; + } + } + return shadow; +} + +float sampleShadowmapPCF(uint shadowmap, mat4 viewProj, vec3 position, LightSettings settings) { + if (settings.ShadowSamples <= 0) { + return sampleShadowmap(shadowmap, viewProj, position, settings.ShadowBias); + } + + vec4 shadowCoord = biasMat * viewProj * vec4(position, 1); + shadowCoord = shadowCoord / shadowCoord.w; + + float shadow = 1.0; + if (shadowCoord.z > -1.0 && shadowCoord.z < 1.0 && shadowCoord.w > 0) { + vec2 texelSize = 1.0 / _shadow_size(shadowmap); + float actual = exp(SHADOW_POWER * (shadowCoord.z - settings.ShadowBias)) / exp(SHADOW_POWER); + + float count = 0.0; + int numSamples = settings.ShadowSamples; + for (int x = -numSamples; x <= numSamples; ++x) { + for (int y = -numSamples; y <= numSamples; ++y) { + vec2 offset = vec2(float(x), float(y)) * texelSize * settings.ShadowSampleRadius; + float dist = _shadow_texture(shadowmap, shadowCoord.st + offset); + + // Compare the difference between exponential depth values + if (dist - actual < 0) { + count += 1.0; + } + } + } + + shadow = 1 - count / float((2 * numSamples + 1) * (2 * numSamples + 1)); + } + return shadow; +} + +float blendCascades(Light light, vec3 position, float depth, float blendRange, LightSettings settings) { + // determine the cascade index + int cascadeIndex = 0; + for (int i = 0; i < SHADOW_CASCADES; ++i) { + if (depth < light.Distance[i]) { + cascadeIndex = i; + break; + } + } + + float shadowCurrent = sampleShadowmapPCF(light.Shadowmap[cascadeIndex], light.ViewProj[cascadeIndex], position, settings); + + // blend with previous cascade to get a smooth transition + if (cascadeIndex > 0 && blendRange > 0) { + float cascadeStart = light.Distance[cascadeIndex - 1]; + float cascadeEnd = light.Distance[cascadeIndex]; + blendRange *= cascadeIndex; + float blendFactor = smoothstep(cascadeStart, cascadeStart + blendRange, depth); + + if (blendFactor > 0) { + float shadowPrev = sampleShadowmapPCF(light.Shadowmap[cascadeIndex - 1], light.ViewProj[cascadeIndex - 1], position, settings); + return mix(shadowPrev, shadowCurrent, blendFactor); + } + } + + return shadowCurrent; +} + +float sqr(float x) +{ + return x * x; +} + +float attenuate_no_cusp(float distance, float radius, float max_intensity, float falloff) +{ + float s = distance / radius; + + if (s >= 1.0) + return 0.0; + + float s2 = sqr(s); + + return max_intensity * sqr(1 - s2) / (1 + falloff * s2); +} + +float attenuate_cusp(float distance, float radius, float max_intensity, float falloff) +{ + float s = distance / radius; + + if (s >= 1.0) + return 0.0; + + float s2 = sqr(s); + + return max_intensity * sqr(1 - s2) / (1 + falloff * s); +} + +/* calculates lighting contribution from a point light source */ +float calculatePointLightContrib(Light light, vec3 surfaceToLight, float distanceToLight, vec3 normal) { + if (distanceToLight > light.Range) { + return 0.0; + } + + // calculate normal coefficient + float normalCoef = max(0.0, dot(normal, surfaceToLight)); + + float attenuation = attenuate_cusp(distanceToLight, light.Range, light.Intensity, light.Falloff); + + // multiply and return light contribution + return normalCoef * attenuation; +} + +vec3 ambientLight(LightSettings settings, float occlusion) { + return settings.AmbientColor.rgb * settings.AmbientIntensity * occlusion; +} + +vec3 calculateLightColor(Light light, vec3 position, vec3 normal, float depth, LightSettings settings) { + float contrib = 0.0; + float shadow = 1.0; + if (light.Type == DIRECTIONAL_LIGHT) { + // directional lights store the direction in the position uniform + // i.e. the light coming from the position, shining towards the origin + vec3 lightDir = normalize(light.Position.xyz); + vec3 surfaceToLight = -lightDir; + contrib = max(dot(surfaceToLight, normal), 0.0); + + float bias = settings.ShadowBias * max(0.0, 1.0 - dot(normal, lightDir)); + position += normal * settings.NormalOffset; + shadow = blendCascades(light, position, depth, light.Range, settings); + +#if DEBUG_CASCADES + int index = -1; + for(int i = 0; i < SHADOW_CASCADES; i++) { + if (depth < light.Distance[i]) { + index = i; + break; + } + } + return contrib * shadow * mix(vec3(0,1,0), vec3(1,0,0), float(index) / (SHADOW_CASCADES - 1)); +#endif + } + else if (light.Type == POINT_LIGHT) { + // calculate light vector & distance + vec3 surfaceToLight = light.Position.xyz - position; + float distanceToLight = length(surfaceToLight); + surfaceToLight = normalize(surfaceToLight); + contrib = calculatePointLightContrib(light, surfaceToLight, distanceToLight, normal); + } + + return light.Color.rgb * light.Intensity * contrib * shadow; +} diff --git a/assets/shaders/lib/ui.glsl b/assets/shaders/lib/ui.glsl new file mode 100644 index 0000000..b81a995 --- /dev/null +++ b/assets/shaders/lib/ui.glsl @@ -0,0 +1,23 @@ +struct Quad { + vec2 min; // top left + vec2 max; // bottom right + vec2 uv_min; // top left uv + vec2 uv_max; // bottom right uv + vec4 color[4]; + float zindex; + float corner_radius; + float edge_softness; + float border; + uint texture; +}; + +UNIFORM(0, config, { + vec2 resolution; + float zmax; +}) +STORAGE_BUFFER(1, Quad, quads) + +float RoundedRectSDF(vec2 sample_pos, vec2 rect_center, vec2 rect_half_size, float r) { + vec2 d2 = (abs(rect_center - sample_pos) - rect_half_size + vec2(r, r)); + return min(max(d2.x, d2.y), 0.0) + length(max(d2, 0.0)) - r; +} diff --git a/assets/shaders/light.fs.glsl b/assets/shaders/light.fs.glsl new file mode 100644 index 0000000..210c46c --- /dev/null +++ b/assets/shaders/light.fs.glsl @@ -0,0 +1,60 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" +#include "lib/lighting.glsl" + +CAMERA(0, camera) +LIGHT_BUFFER(1, lights) +SAMPLER(2, diffuse) +SAMPLER(3, normal) +SAMPLER(4, position) +SAMPLER(5, occlusion) +SAMPLER_ARRAY(6, shadowmaps) + +IN(0, vec2, texcoord) +OUT(0, vec4, color) + +vec3 getWorldPosition(vec3 viewPos); +vec3 getWorldNormal(vec3 viewNormal); + +void main() { + // unpack data from geometry buffer + vec3 viewPos = texture(tex_position, in_texcoord).xyz; + vec3 viewNormal = unpack_normal(texture(tex_normal, in_texcoord).xyz); + + vec4 gcolor = texture(tex_diffuse, in_texcoord); + vec3 diffuseColor = gcolor.rgb; + float occlusion = gcolor.a; + + vec3 position = getWorldPosition(viewPos); + vec3 normal = getWorldNormal(viewNormal); + + float ssao = texture(tex_occlusion, in_texcoord).r; + if (ssao == 0) { + ssao = 1; + } + + // accumulate lighting + vec3 lightColor = ambientLight(lights.settings, occlusion * ssao); + int lightCount = lights.settings.Count; + for(int i = 0; i < lightCount; i++) { + lightColor += calculateLightColor(lights.item[i], position, normal, viewPos.z, lights.settings); + } + + // linearize gbuffer diffuse + vec3 linearDiffuse = pow(diffuseColor, vec3(2.2)); + + // write shaded fragment color + out_color = vec4(lightColor * linearDiffuse, 1); +} + +vec3 getWorldPosition(vec3 viewPos) { + vec4 pos_ws = camera.ViewInv * vec4(viewPos, 1); + return pos_ws.xyz / pos_ws.w; +} + +vec3 getWorldNormal(vec3 viewNormal) { + vec4 worldNormal = camera.ViewInv * vec4(viewNormal, 0); + return normalize(worldNormal.xyz); +} diff --git a/assets/shaders/light.json b/assets/shaders/light.json new file mode 100644 index 0000000..c8498b2 --- /dev/null +++ b/assets/shaders/light.json @@ -0,0 +1,21 @@ +{ + "Inputs": { + "position": { + "Index": 0, + "Type": "float" + }, + "texcoord_0": { + "Index": 1, + "Type": "float" + } + }, + "Bindings": { + "Camera": 0, + "Lights": 1, + "Diffuse": 2, + "Normal": 3, + "Position": 4, + "Occlusion": 5, + "Shadow": 6 + } +} diff --git a/assets/shaders/light.vs.glsl b/assets/shaders/light.vs.glsl new file mode 100644 index 0000000..e7103e4 --- /dev/null +++ b/assets/shaders/light.vs.glsl @@ -0,0 +1,19 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" + +IN(0, vec3, position) +IN(1, vec2, texcoord) +OUT(0, vec2, texcoord) + +out gl_PerVertex +{ + vec4 gl_Position; +}; + +void main() +{ + out_texcoord = in_texcoord; + gl_Position = vec4(in_position, 1); +} diff --git a/assets/shaders/lines.fs.glsl b/assets/shaders/lines.fs.glsl new file mode 100644 index 0000000..e628abc --- /dev/null +++ b/assets/shaders/lines.fs.glsl @@ -0,0 +1,20 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" + +IN(0, vec3, color) +OUT(0, vec4, color) + +float FogDensity = 0.04; + +void main() +{ + float depth = gl_FragCoord.z / gl_FragCoord.w - 0.2; + + // Calculate the fog factor + float fogFactor = exp(-depth * FogDensity); + fogFactor = clamp(fogFactor, 0.0, 1.0); + + out_color = vec4(in_color, fogFactor); +} diff --git a/assets/shaders/lines.json b/assets/shaders/lines.json new file mode 100644 index 0000000..3283303 --- /dev/null +++ b/assets/shaders/lines.json @@ -0,0 +1,18 @@ +{ + "Inputs": { + "position": { + "Index": 0, + "Type": "float" + }, + "color_0": { + "Index": 1, + "Type": "float" + } + }, + "Bindings": { + "Camera": 0, + "Objects": 1, + "Lights": 2, + "Textures": 3 + } +} diff --git a/assets/shaders/lines.vs.glsl b/assets/shaders/lines.vs.glsl new file mode 100644 index 0000000..c1fcbab --- /dev/null +++ b/assets/shaders/lines.vs.glsl @@ -0,0 +1,24 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" + +CAMERA(0, camera) +STORAGE_BUFFER(1, Object, objects) + +IN(0, vec3, position) +IN(1, vec4, color) +OUT(0, vec3, color) + +out gl_PerVertex +{ + vec4 gl_Position; +}; + +void main() +{ + out_color = in_color.rgb; + + mat4 mvp = camera.ViewProj * objects.item[gl_InstanceIndex].model; + gl_Position = mvp * vec4(in_position, 1); +} diff --git a/assets/shaders/old/billboard.fs.glsl b/assets/shaders/old/billboard.fs.glsl new file mode 100644 index 0000000..cf55ea2 --- /dev/null +++ b/assets/shaders/old/billboard.fs.glsl @@ -0,0 +1,11 @@ +#version 330 + +uniform sampler2D sprite; + +in vec2 uv; +layout(location=0) out vec4 color; + +void main() +{ + color = texture(sprite, uv); +} \ No newline at end of file diff --git a/assets/shaders/old/billboard.gs.glsl b/assets/shaders/old/billboard.gs.glsl new file mode 100644 index 0000000..7762d94 --- /dev/null +++ b/assets/shaders/old/billboard.gs.glsl @@ -0,0 +1,43 @@ +#version 330 + +layout (points) in; +layout (triangle_strip) out; +layout (max_vertices = 4) out; + +uniform mat4 m; +uniform mat4 vp; +uniform vec3 eye; + +out vec2 uv; + +void main() +{ + vec3 pos = gl_in[0].gl_Position.xyz; + + vec3 toCamera = normalize(eye - pos); + vec3 up = vec3(0.0, 1.0, 0.0); + vec3 right = cross(toCamera, up); + + pos -= (right * 0.5); + gl_Position = vp * vec4(pos, 1.0); + uv = vec2(0.0, 0.0); + EmitVertex(); + + pos.y += 1.0; + gl_Position = vp * vec4(pos, 1.0); + uv = vec2(0.0, 1.0); + EmitVertex(); + + pos.y -= 1.0; + pos += right; + gl_Position = vp * vec4(pos, 1.0); + uv = vec2(1.0, 0.0); + EmitVertex(); + + pos.y += 1.0; + gl_Position = vp * vec4(pos, 1.0); + uv = vec2(1.0, 1.0); + EmitVertex(); + + EndPrimitive(); +} \ No newline at end of file diff --git a/assets/shaders/old/billboard.vs.glsl b/assets/shaders/old/billboard.vs.glsl new file mode 100644 index 0000000..e437a8e --- /dev/null +++ b/assets/shaders/old/billboard.vs.glsl @@ -0,0 +1,10 @@ +#version 330 + +layout (location=0) in vec3 position; + +uniform mat4 model; + +void main() +{ + gl_Position = model * vec4(position, 1.0); +} diff --git a/assets/shaders/output.fs.glsl b/assets/shaders/output.fs.glsl new file mode 100644 index 0000000..60c3164 --- /dev/null +++ b/assets/shaders/output.fs.glsl @@ -0,0 +1,13 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" + +IN(0, vec2, texcoord) +OUT(0, vec4, color) +SAMPLER(0, diffuse) + +void main() +{ + out_color = vec4(texture(tex_diffuse, in_texcoord).rgb, 1); +} diff --git a/assets/shaders/output.json b/assets/shaders/output.json new file mode 100644 index 0000000..377d56e --- /dev/null +++ b/assets/shaders/output.json @@ -0,0 +1,15 @@ +{ + "Inputs": { + "position": { + "Index": 0, + "Type": "float" + }, + "texcoord_0": { + "Index": 1, + "Type": "float" + } + }, + "Bindings": { + "Output": 0 + } +} \ No newline at end of file diff --git a/assets/shaders/output.vs.glsl b/assets/shaders/output.vs.glsl new file mode 100644 index 0000000..e7103e4 --- /dev/null +++ b/assets/shaders/output.vs.glsl @@ -0,0 +1,19 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" + +IN(0, vec3, position) +IN(1, vec2, texcoord) +OUT(0, vec2, texcoord) + +out gl_PerVertex +{ + vec4 gl_Position; +}; + +void main() +{ + out_texcoord = in_texcoord; + gl_Position = vec4(in_position, 1); +} diff --git a/assets/shaders/postprocess.fs.glsl b/assets/shaders/postprocess.fs.glsl new file mode 100644 index 0000000..f13570d --- /dev/null +++ b/assets/shaders/postprocess.fs.glsl @@ -0,0 +1,54 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" + +IN(0, vec2, texcoord) +OUT(0, vec4, color) +SAMPLER(0, input) +SAMPLER(1, lut) + +#define MAXCOLOR 15.0 +#define COLORS 16.0 +#define WIDTH 256.0 +#define HEIGHT 16.0 + +vec3 lookup_color(sampler2D lut, vec3 clr) { + float cell = clr.b * MAXCOLOR; + + float cell_l = floor(cell); + float cell_h = ceil(cell); + + float half_px_x = 0.5 / WIDTH; + float half_px_y = 0.5 / HEIGHT; + float r_offset = half_px_x + clr.r / COLORS * (MAXCOLOR / COLORS); + float g_offset = half_px_y + clr.g * (MAXCOLOR / COLORS); + + vec2 lut_pos_l = vec2(cell_l / COLORS + r_offset, 1 - g_offset); + vec2 lut_pos_h = vec2(cell_h / COLORS + r_offset, 1 - g_offset); + + vec3 graded_color_l = texture(lut, lut_pos_l).rgb; + vec3 graded_color_h = texture(lut, lut_pos_h).rgb; + + return mix(graded_color_l, graded_color_h, fract(cell)); +} + +void main() { + // todo: expose as uniform setting + float exposure = 1.0; + + // get input color + vec3 hdrColor = texture(tex_input, in_texcoord).rgb; + + // exposure tone mapping + vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure); + + // gamma correction + vec3 corrected = pow(mapped, vec3(1/gamma)); + + // color grading + vec3 graded = lookup_color(tex_lut, corrected); + + // return + out_color = vec4(graded, 1); +} diff --git a/assets/shaders/postprocess.json b/assets/shaders/postprocess.json new file mode 100644 index 0000000..a43a243 --- /dev/null +++ b/assets/shaders/postprocess.json @@ -0,0 +1,16 @@ +{ + "Inputs": { + "position": { + "Index": 0, + "Type": "float" + }, + "texcoord_0": { + "Index": 1, + "Type": "float" + } + }, + "Bindings": { + "Input": 0, + "LUT": 1 + } +} diff --git a/assets/shaders/postprocess.vs.glsl b/assets/shaders/postprocess.vs.glsl new file mode 100644 index 0000000..e7103e4 --- /dev/null +++ b/assets/shaders/postprocess.vs.glsl @@ -0,0 +1,19 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" + +IN(0, vec3, position) +IN(1, vec2, texcoord) +OUT(0, vec2, texcoord) + +out gl_PerVertex +{ + vec4 gl_Position; +}; + +void main() +{ + out_texcoord = in_texcoord; + gl_Position = vec4(in_position, 1); +} diff --git a/assets/shaders/shadow.fs.glsl b/assets/shaders/shadow.fs.glsl new file mode 100644 index 0000000..dd3c412 --- /dev/null +++ b/assets/shaders/shadow.fs.glsl @@ -0,0 +1,13 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" +#include "lib/lighting.glsl" + +IN(0, float, depth) + +void main() +{ + // exponential depth + gl_FragDepth = exp(SHADOW_POWER * in_depth) / exp(SHADOW_POWER); +} diff --git a/assets/shaders/shadow.json b/assets/shaders/shadow.json new file mode 100644 index 0000000..f706ee5 --- /dev/null +++ b/assets/shaders/shadow.json @@ -0,0 +1,14 @@ +{ + "Inputs": { + "position": { + "Index": 0, + "Type": "float" + } + }, + "Bindings": { + "Camera": 0, + "Objects": 1, + "Lights": 2, + "Textures": 3 + } +} diff --git a/assets/shaders/shadow.vs.glsl b/assets/shaders/shadow.vs.glsl new file mode 100644 index 0000000..26ee5a4 --- /dev/null +++ b/assets/shaders/shadow.vs.glsl @@ -0,0 +1,25 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" + +CAMERA(0, camera) +STORAGE_BUFFER(1, Object, objects) + +// Attributes +IN(0, vec3, position) +OUT(0, float, depth) + +out gl_PerVertex +{ + vec4 gl_Position; +}; + +void main() +{ + mat4 mvp = camera.ViewProj * objects.item[gl_InstanceIndex].model; + gl_Position = mvp * vec4(in_position, 1); + + // store linear depth + out_depth = gl_Position.z / gl_Position.w; +} diff --git a/assets/shaders/ssao.fs.glsl b/assets/shaders/ssao.fs.glsl new file mode 100644 index 0000000..ae279e9 --- /dev/null +++ b/assets/shaders/ssao.fs.glsl @@ -0,0 +1,73 @@ +#version 450 core +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" + +IN(0, vec2, texcoord) +OUT(0, float, ssao) + +#define KERNEL_SIZE 32 +layout (std140, binding = 0) uniform Params { + mat4 Projection; + vec4 Kernel[KERNEL_SIZE]; + int Samples; + float Scale; + float Radius; + float Bias; + float Power; +}; + +SAMPLER(1, position) +SAMPLER(2, normal) +SAMPLER(3, noise) + +void main() +{ + vec2 noiseSize = vec2(textureSize(tex_noise, 0)); + vec2 outputSize = vec2(textureSize(tex_position, 0)) / Scale; + vec2 noiseScale = outputSize / noiseSize; + + // get input vectors from gbuffer & noise texture + vec3 fragPos = texture(tex_position, in_texcoord).xyz; + vec3 normalEncoded = texture(tex_normal, in_texcoord).xyz; + vec3 normal = unpack_normal(normalEncoded); + + // discard gbuffer entries without normal data + if (normalEncoded == vec3(0)) { + out_ssao = 1; + return; + } + + vec3 randomVec = texture(tex_noise, in_texcoord * noiseScale).xyz; + + // create TBN change-of-basis matrix: from tangent-space to view-space + vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal)); + vec3 bitangent = cross(normal, tangent); + mat3 TBN = mat3(tangent, bitangent, normal); + + // iterate over the sample kernel and calculate occlusion factor + float occlusion = 0.0; + for(int i = 0; i < Samples; ++i) + { + // get sample position + vec3 sampleVec = TBN * Kernel[i].xyz; // from tangent to view-space + sampleVec = fragPos + sampleVec * Radius; + + // project sample position (to sample texture) (to get position on screen/texture) + vec4 offset = vec4(sampleVec, 1.0); + offset = Projection * offset; // from view to clip-space + offset.xyz /= offset.w; // perspective divide, clip -> NDC + offset.xyz = offset.xyz * 0.5 + 0.5; // transform to range 0.0 - 1.0 + + // get sample depth - i.e. the Z component of the sampled position in view space + float sampleDepth = texture(tex_position, offset.xy).z; + + // range check & accumulate + float rangeCheck = smoothstep(0.0, 1.0, Radius / abs(fragPos.z - sampleDepth)); + occlusion += (sampleDepth <= sampleVec.z - Bias ? 1.0 : 0.0) * rangeCheck; + } + occlusion = 1.0 - (occlusion / Samples); + occlusion = pow(occlusion, Power); + + out_ssao = occlusion; +} diff --git a/assets/shaders/ssao.json b/assets/shaders/ssao.json new file mode 100644 index 0000000..e410d5e --- /dev/null +++ b/assets/shaders/ssao.json @@ -0,0 +1,18 @@ +{ + "Inputs": { + "position": { + "Index": 0, + "Type": "float" + }, + "texcoord_0": { + "Index": 1, + "Type": "float" + } + }, + "Bindings": { + "Params": 0, + "Position": 1, + "Normal": 2, + "Noise": 3 + } +} diff --git a/assets/shaders/ssao.vs.glsl b/assets/shaders/ssao.vs.glsl new file mode 100644 index 0000000..e7103e4 --- /dev/null +++ b/assets/shaders/ssao.vs.glsl @@ -0,0 +1,19 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" + +IN(0, vec3, position) +IN(1, vec2, texcoord) +OUT(0, vec2, texcoord) + +out gl_PerVertex +{ + vec4 gl_Position; +}; + +void main() +{ + out_texcoord = in_texcoord; + gl_Position = vec4(in_position, 1); +} diff --git a/assets/shaders/ui_quad.fs.glsl b/assets/shaders/ui_quad.fs.glsl new file mode 100644 index 0000000..d7159f4 --- /dev/null +++ b/assets/shaders/ui_quad.fs.glsl @@ -0,0 +1,56 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" +#include "lib/ui.glsl" + +SAMPLER_ARRAY(2, textures) + +IN(0, vec4, color) +IN(1, vec2, texcoord) +IN(2, vec2, position) +IN(3, flat vec2, center) +IN(4, flat vec2, half_size) +IN(5, flat uint, quad_index) +OUT(0, vec4, color) + +void main() +{ + Quad quad = quads.item[in_quad_index]; + + // shrink the rectangle's half-size that is used for distance calculations + // otherwise the underlying primitive will cut off the falloff too early. + vec2 softness_padding = vec2(max(0, quad.edge_softness*2-1), + max(0, quad.edge_softness*2-1)); + + // sample distance to rect at position + float dist = RoundedRectSDF(in_position, + in_center, + in_half_size-softness_padding, + quad.corner_radius); + + // map distance to a blend factor + float sdf_factor = 1 - smoothstep(0, 2*quad.edge_softness, dist); + + float border_factor = 1.f; + if(quad.border > 0) + { + vec2 interior_half_size = in_half_size - vec2(quad.border); + float interior_radius = quad.corner_radius - quad.border; + + // calculate sample distance from interior + float inside_d = RoundedRectSDF(in_position, + in_center, + interior_half_size- + softness_padding, + interior_radius); + + // map distance => factor + float inside_f = smoothstep(0, 2*quad.edge_softness, inside_d); + border_factor = inside_f; + } + + vec4 sample0 = texture(textures[quad.texture], in_texcoord); + out_color = in_color * sample0 * sdf_factor * border_factor; +} + diff --git a/assets/shaders/ui_quad.json b/assets/shaders/ui_quad.json new file mode 100644 index 0000000..83dab87 --- /dev/null +++ b/assets/shaders/ui_quad.json @@ -0,0 +1,13 @@ +{ + "Inputs": { + "position": { + "Index": 0, + "Type": "float" + } + }, + "Bindings": { + "Config": 0, + "Quads": 1, + "Textures": 2 + } +} \ No newline at end of file diff --git a/assets/shaders/ui_quad.vs.glsl b/assets/shaders/ui_quad.vs.glsl new file mode 100644 index 0000000..508405d --- /dev/null +++ b/assets/shaders/ui_quad.vs.glsl @@ -0,0 +1,47 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" +#include "lib/ui.glsl" + +OUT(0, vec4, color) +OUT(1, vec2, texcoord) +OUT(2, vec2, position) +OUT(3, flat vec2, center) +OUT(4, flat vec2, half_size) +OUT(5, flat uint, quad_index) + +out gl_PerVertex +{ + vec4 gl_Position; +}; + +const vec2 vertices[] = +{ + {-1, -1}, + {-1, +1}, + {+1, -1}, + {+1, +1}, +}; + +void main() +{ + out_quad_index = gl_InstanceIndex; + Quad quad = quads.item[out_quad_index]; + + out_half_size = (quad.max - quad.min) / 2; + out_center = (quad.max + quad.min) / 2; + out_position = vertices[gl_VertexIndex] * out_half_size + out_center; + + vec2 tex_half_size = (quad.uv_max - quad.uv_min) / 2; + vec2 tex_center = (quad.uv_max + quad.uv_min) / 2; + out_texcoord = vertices[gl_VertexIndex] * tex_half_size + tex_center; + + gl_Position = vec4( + 2 * out_position.x / config.resolution.x - 1, + 2 * out_position.y / config.resolution.y - 1, + 1 - quad.zindex / (config.zmax + 1), + 1); + + out_color = quad.color[gl_VertexIndex]; +} diff --git a/assets/textures/color_grading/none.png b/assets/textures/color_grading/none.png new file mode 100644 index 0000000000000000000000000000000000000000..b4a61edfb799e83f7c2f5b08aece7d9864eb0643 GIT binary patch literal 646 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 COo)yE literal 0 HcmV?d00001 diff --git a/assets/textures/heightmap.png b/assets/textures/heightmap.png new file mode 100644 index 0000000000000000000000000000000000000000..9991a742f69d543f55e50c6ca75eaaac2f183c6f GIT binary patch literal 37173 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 literal 0 HcmV?d00001 diff --git a/assets/textures/uv_checker.png b/assets/textures/uv_checker.png new file mode 100644 index 0000000000000000000000000000000000000000..0fdaba8ba615580de5fbad68b35ecb766720a7d0 GIT binary patch literal 28810 zcmeFZXIK;8zc0Ebh2FcM5=9UM1Vx&(U;{y7jX&R_t45n+Ck{K`jr`&5vhe2e!ssIr5s^>#NuMa; z6(=lxE%sVmJEv@<>#nZsdy<%ud~(0L{k;PQ8J8?~tIF?^k>0H!E4_8|WX{DdFUQHB z2@W?yTHWnvUvJ$Ax7}-X#L-06HtH(H`S^9cz2(sk-A}x`TcdA~Ocb;<4xIBm9u{zD zuL3RW+##Z>)lP*iDoRF#EiWt&I@;=_95E@+znGVKHZRo1_>^j6ivJLnhMnECBhmw;@X00@XaddT!daR1D+xa%)1dM|9T>Cpf2 z*!xH67-3>F4arMT7Qx_d-Sc-^dMkZ;V6MuvN`LUmIlD_`71^x5?(pfe#+BOgbYTt; zzyJH@??V==d!^vxUkoo+ z;Iopo6AdmvXQe-nxVix<60k`7v}ydla9!kswz1 zuxeRN%He(a#kElDtu^WG#$Vdh5_Fuv2i8QbnUg+wB6Z;L9r0|oJ!EESgq$3!vB?d8 z%a^6b!QL&IX!I=K8o3pIa1E(gTsLhmNj&w`7C208^`H2-*7j3(Ru6CU_M+JQZOg^b zo2uX!TX62aeH&TwhMDM%ay!lu>V3O*Wa?}E;+XVHuwWb9W)KjruEWqA#5XY(8DI^#t?A&2gP155*>8&RU`&{obLoJa?G^!>^$Yp}c}dViF) z^zz~3P+t|}8fIrbnJuRS_31K9#;otX;v#TS>l?3K#lqI$Is1wqPUJH=q*lM3iZ%tU z>4Bj^a-SkzDzCCZSeoQ*mD%^&OSI1mlF>B@-?ix?W( z8X5mn_z5nM!Ys4;F3ssc%9eUB?&YUnpZ}QFzOF@Mee~(1UBpIe^3pHP`=$+2?P;vu zBNb!eRL$jgIIqh(HBl)iW16mcLO14V{d=*2pN)BBT4+n_#YV@OE${*6S_`6FMAT>F zSKcobku_grh?q#g`mH-d!?;b2sN)d{ED$ZO`Lvq-Ym@;Iz3L_7i@N0 z=n2kC=kMnn_)Gj|g;V0_MfeALB}T|<-lZ&wXeNFXnozS*CU<^=zw$h4B<%y^vz}gw zX5fr2c`A6`#JZk;{3Y_Prdb`7an3+|KC&Oj8jt^U7y>#Y-mK9icJc-Fifzr>J z#bzWtRtFrpb_BYyAic9~8gq+|C~ZY_oA%&YY<0_h-xqYPyRjDtDM-|hj-R2rl<9ia zxg#7eeTk->FlX4B(!Z{Gm~!&c9*_f@ zTBL}24?UqS7i`Aol7JAxBdvrPfkXm}@9DbaKuqz7p65z;AnTq8H!fGC^$JBzfN zV{qC|-i8l(zM@JwSLA#dGK{}IWY4rCoPkmYYaev?{oLo!BnC@=c~2H}=NolVZP@{H zH?UOCtC%0&w=RPh=`fLgYj_Ng7+;u7xlcJuNY5Y3v{eKJD?uX0?Tu>aMlY?aZ$upK zUQ0WCeDSqR4RD+*mR^w5l>^tN&QW*3ft-~WSE%af8rwgrp@2LV9Xv><%M)PHcrZOr zE6FBKV@gy4WX8}+j5u=T60JWmwIjT+QLS95MDBJwsy>%U2j$q!;24*c??6)_blf`*0&-yQib|p@xreDGle1zxDU>{>bTHq&`q_-bK`J=?^ zUO-*r5=0!ba^oR*z%)_109q#~)_)-LVU07K{@HH&K_N&@8`)efaf1ZfzGQxE4Pf55 zAQGs*x+vBV+)^!a7Tpr5mdHIND1`QOH0AdVl`3>x%Ct>dTqN7JBMs|aCW+1}%UY1y z1o?H8r706%l8(H){kKj=A#{|7dYR+9`K`o`wrYuBeMC#x_4hWZm?d-wX~-;Hbuvbd zeoz5ioS<^9tQ{Pq-c_2$r@i1Qc)IL9cXaZVMltYP<`ZdHQ}@f&i33hbO{v-a3{mNkAHnWRNZ#4A>2Q`1;(MsUOu zyIIWUtyHzs&d*R6v$zMAHMKPD!XWm$JkMqK%7(XF1(a`8U!ntBE(M0c37G<_@x zjzEVPK{NOPgCBMs6lEzM{jjKbaVhBhcmR3)xDm4aPLrCl)Tn4}nwn?C<-NpQ_=FEU zod~&5<3$UarOtWrQ2Shlp9rS@k#|az-5#0wXwF0tT=A{*J?XK~#syx2?M&-IxnCJI z`K)+-_@5_>B%8F{i8)v9{okk*-wW6^9i%fPAnh;+6I#C9S1RlX32dIuzID6a?5iWN z012C?yWwoY^}Ji(_P8A_xgS$kioP1>fab>S3esX+a#AP%V%xPZk!+?^Cn)`DCB;$5 zi-^E_b{n~>Dg+n!>)4WE>^)WqAN^P%_DT!u>#r?`GeSX5$L`EtwBkx2tD&_Ua^J5| zNAf3K&Vi`P=o>sNM~eup^#4#>Fs30z)~J@29>F{mj`5dLeo8Lg3afDnbO7G?9yv(+ z?-h|siCgyIS$CJU30sAH&|OSS-oan{uNFT&-O5oy(>QY$T+GuS+WouxwxNnLz*VkR|I9r-6I%AccMrxJykR6yVC#hzw4WgPqyWet+oej`C)#@WXcE7D|Y9s4$U> z?EjFOZ$`Lkld>CLS?Zs#z%$-PB2+)P$?D{*XM}o%@DgQ?CgI%9TXQ@SI_JZqyrMzV zFhv&Esxn%92&A#91pcj>XvRR5zMg^`82@`*i(#cxfSfE-B=yae8nk6}_*NsByT5 zJZTY4{4bnwT`SA00q8L7@kI*yhN1tQh>)%yjdzqtvJ>g)o~pU7jfg~NGs7{}^9gG?_vrbWqxm9_o)LD}nHi3U z;51hbTo+y|*P3m-(=r_Q{5xnGnL8o0@5BOmQE)Yz_loHPv$Iv>W^xlP^z_*lT~FsE zHQ1N-Ie_6VoC(;6Gkg9-Wp@xhFCw(LnCQtBCKc%F9kb{x{Q zvai}F72o$6*?m-)$VXPh=q=l}bMMCg5~n85@ss}IKe%H4wm6Emkp8I4y&sD673m^P z${PnwuH%qp`<>I4*$f006B{+fd4>B3U zHr<6gB1G-ynX}BDb9oK@ylvx_OLXj zxU_35ShV0b*Oh&qXU~(5e#a(#AkU_8v6UX-Ur*!akIme&{dL+0!|43W2!fY56WPYJ|d4Ch1`|M zUksg>xPNjzSJU*_dFyTckmVC_$Dg0v;qJTt_8Fi8wZ2hj>s@=M|2Kfk;vXU6$s(@L zKCS#8f^j14WNoRgh#YM&%BW&{RdMfswdI!`Q$sFN<2>fpxXHrZ)d=xW@D|_YzPKLN zby-*k`rgC09#Ngaduqk3Xw>-PC|B0o7I}rNVzqnP;tJNV!m1jJS_nU{K;LU#%ukm`V|k z8g^SfVu*C^ui3@=DQez@O==>sPr2`YOm-*9K9a^ZOH$Y0_hM6`19|D!BT^FmRm^XD zfS{?hT6as+aJgogPtl}e%JjC=xO&1}Gw_8ycX~!_bY$)`Ux@=gVO(BYsqhxk8t=8c z245|$Iptryt0|D5FgaFy(|+4c5-_b867U@gntrz^deDVoOD!a~W@t!3hZbp9kfk(h zw|v%G5Pyc1cOrCn5iu1e$|8688+@iG$mbz}O1Hp(xc>x~4-iGo57(nJhc>zF`#}NNN{MWyCjnpdw#qxak)0e*Uznscg1?dAO z6N}^PX{@?6>=KkZU@R3szV6@LaDfy7gyuX;T^2d_roRjxt3#;@r-eif0MYdMo=}CC z_QuET1R;}9$U$?<2?Mk`o*b}d_Cw%Bo{?6z{Ey-E83$`LieV z@ixj5yJzI?u``*a#esZ;%uWr;pRfXdYpWHL_N_00lr>9hO(&+i;=$Uo)0QKUci9j7 zHDfc=%%_`bF+5`=Ck5Dx(_vc~W8?UV^c);uC3>t31-#`Y?qi4c#0*t0S3=Kx(b)V@ z;~cdme9qlJ6q#S?!x3a7*X^tZ#(UoBXo|_fQs4_C%|UM1IOwDxFh(f{-A6ox8%U{i zaBXdbiwgM~dMcCU>mtGUlDa`->6fN6-QCM)uZ3NN)MP`Wua?!+R_<)M`Dx5(?YE|n zB=tojS@B3ry2ff6t3o5$!U?LwvLNTHe^(B>1$g|8yNQ8t^PS2P)cxeC)csgon0_`p z2B&HJi8k!{Onp>j^?Z^FJo7x5>+`et-X5*zZ_I()POHup_CLQ_w0I<1sSI4-IlL)w z+%5!e@X4pVvLa zR4d$%sE7;>%2@~#g%Ih*sz!ob2`_b}kfO6&tHUPg4!GL>%1m+@Y7aRhdO3!WHr<^w-h8Lz2?@^W-Hm*Cv(d3+rW*!J9EuB=HR67kS z58y>E9XKc+*=ZX8O z@gfn9MIc=M#1;^NF~9vfb1M|Akv|b|9C5#j;Gu!1(yM+fxj*_#9^kc!k#JfYd_;UYxJ z{Sk+mGdtKZ+@B%kn}SP2xxB<4quHAM$##n9S*Kn-1ecj6iWhFT5mzDRz4uG!gruu{P%b`fTd2_0C?^$Rv-G_%?UY{y@1C0qW2ejgMGa zbrkh#JPt9rwVt(hTjM=A1 zh$ny8in4$x5x{Hg0bW{Ummd32!39kxw~hIT&0W5a7NzR#0-cuN1=EbGO&dvSE4gUGqNc$H*Vp{WSrTTAeR$2Usv zfZ_^_fuliI>t@&|uCr`+;ro}zP)0qaB6{nIg_Umjw~mT`gU`Yo)315gNB|j;K9XxE z$atAeO?jGvA>qhpKkWu2-%X%huK*@{LBZ8%OB12g?vsN&SvFgseu01Nw4tR{I-=?-r*YOkv&ce#b3ca02?zn-+la!@ytTOkpm1`R2auB&( zzE+}gs`yBlz>Yy-PY=o1#eG167oEf|J9x4CffzF-!kV4FG4ssroP=8G0YB*FW1iwo zB+*>yzOVK>;jCYr37pT#2V7b5%dg0e%6tcWQsVRiI8;^G^QAL^HXroLFiu89huozIb1TNc zZ)i0fQ;jIGNdsacLd_oE188q3^bpe)a3h)XTlo(rJ=7r5;Epd8 z%`%Xm&KsrueZ$#ryNAD|h5R6Q<6^c#w`}R}$6espb9>jDHBIF3LaQ07vbglvI?VC= zAet5MvsfTcbqtv;!aR9-$W#{bHUgW_Q)Zh)1>Ad}4*R!Hf!Vq(lrYzAC7IlGo%37Z z7w4!o)D&#KIQ3fJepvs4E=nq`3_aWC^Xan9Ikx}pja!iu9ut{Ya@O2~H+Yk?rz?&g` z4{{1%B-lwY%>uaJR8{bLb!v_J)@gW-#c$&}(%Moic7ca(77!<>w*HziLtVPwx;N`e zGuxJ-wF+OrSNc3xmd_lw@Yac6(o|XGi%;`q z;%j0N_x4z%)U|b&K%vjCUgq3a8n;3Ug&9E}?-yuV@`OX5+muWhZ<;V#!kisnWSw5) zd~CZoWUA%0Nx&NTHJ_UQRNv@8^WgsA?6EBw{~AwqU!bV1mz&sYpdsjM;oZ_sf0kp> z(epTN_v?2%3p~62d2GRH!)Q7WZO;MhqAFVbL6!u=9%!fm84Oq~;ED#lOW z%cuZD^xO7++AMgtZ zN&rbAy)A!SPk*)(4;+1f9PIlkFM}_cctBa1(eT4*2L6M5F=+37S9iIdqw5aoUH%6d z(`H|BNA;x4P2DMy4f46gTo0(U4BpU-7ejq;(&V2)5suJ#IxRn=pM<>pArudebq$;U zm1SmfQ`xWM6@FL}$eY2H`27mFxWs{k9iw-Zq3)V{guH;YQxhdq? zdiRcN6%6Ze^HH+i8^@1vhd6FOpC+oee|$53+H4Wxkkd?X|EY5Ee-8`)7pT?{5e3Ho z_wyn-C?&8RrXxYDga3t!`hQ^;{dXx5YRiabvc+!!s^-IgCIzTRcc*;Y1Rvf1tVabf z5OuaVPu8^*?WRksd%uao%hwv-BVz}x&A|JWkSUIIC-;6#*E@CB6_c>(Db%EZC~>@@ z@_fNYeLG0_=D{IQuDaB{?qMBu%;$u%q5DcTwo6^tX@z8j7n;pnWQDVC8-Ztb7p@^o zIYjdOS@R$n^wl>RGjjdJDwgH$P5xk`g1A$^ICPTjR+XhJ13qK)3_S*i&N53cd1rhs zr_P*r72Fu(v)w5XOa@E$+@{=o;NnP zxh^g=RffrwgUpU=BUNLuWxB_q%{l|*HtSC9wJ+3N@X5glBw+1)^J0LBuLSkzw}_me zwev#}mlN&5!>4PG^8UR60vqzT$%!54ICgx>OXAC3`be=2s_S#V`Gz%y`q*-2#qno` z2072BzD_NCp{js1a>U;X8QgngN@eFNXMW;5ri7)tk|9-8yEZcRjq}0$BD{m`A5(Ss zAhP^Nry8Q}gl#w~+8Ca3iedd9t}=A~D-b6tBT*CMio2n5HICVoQEg-4m7pU~x%!8f zm2nq*_{DZ(IyMymH2#*oPBoA^Uz5pc=0c=s_p2od-x{nT2~$EMw&! z^2If3FOz>(d&&)w8PI*S9TYrU5$oDpUe5_q@r1maE^LkFij3Gp55e%1w_LIL7RWm2 z5cJVH7WegMPa3@(AO03%#ev(AW`joyYCpoiyC1JW~t|H40A z#zWkO{?4&(I=+xmtUtp`G;SI3_>1{uh!Oclar&a(WO~mU4Pt|q zrJeEzFt>&g#jVKV%!MP+=xOb_W%CW%zIZ?jQ}v~&NuGxwIaIT@ue5AzXCE%u?C=Q(RYKo%g5&;;1KT?k9c&pV5}z{VVlS4mj0>qjQIemd2L7{DeEmEn}pu zk?e0VjkTbg%twytHtl!^WKSv(hkS8q;LkjP-x?L>HW^}M7^!qo;)R1h-|X1E$csv# zptDXwFPfaV|8H9hNkGCgcP!IU0Tke?Us{1DskHOEA!l`}bLOev7<4_!MQjh1E#X1W z=rKu$WjP9eMm6M+i|LmR{KLlLG`&QK*CJ*z?VzTafh)XO)1GV6gP7V09^$!1_VJ^y z-~a)_9ddGq2g?5gnLPnq+ZiUdZIqFaaqH?9&?FP+kh!r`%mBMLVUiNH>nR_l;8XjY zzko?puDyf!4j)CkTmFGA@Ln`gS_ZEe(puy#D}D^#7$BSG6#SNtFs?V3PEHNxBko7S{IIBhqTYXfPVT}cVmQdo{{<{j-&YimMXHV|qrrBgqXFDCnfO-CZ@D-4eTw zSw@ZAgSOhs!Izow^AIY%lN8z^8+=!n8oWB*>9JQG-*I7RWRGMUNURBmaxeG+P5vxV zMQ;C>UB418bMSA8T|fBUWlDLyzVT7S|A>_A;5<{kwA8-(tFUn!eCe)+ZVniqUtO+~ zX^TahBWu)PqCV&1);x5MhV((zx6_9_bT%Z9nfKsEAZ#;?4lAW6nO9I%L5Me*;|#VE$e} zU41&2MHArI_p3%}Oa0^5i(z#OmP)^+?6vixV;D;XX{YmQDbZEKHzG1sskVEju7Z)V)L*ift$^MncaQN`M4L$Mz0c=B(GHOdkrjR&&gaA^ld zcjMl+VDzqIZ))H!>4Lv%I9Gp6dj2Shp}sELJg{$j8bukuP~W@*&W$Bvd;gfsUnyGL zi6=b1(eu+4PvDAHNsK}x#X-=e0Y5e^5F{K0n@GN=M)&q z7E;vX1HAQW-@>2+tO8N$5srt#QDOQ7yXZ1-s&8*N$HMdp7x5My0TXhSEWtc~>Mckw zq3WBK*=JK!4v+mjpD#uf;1q7~)3yq*6+372Ue;x%+ZtlAEiFcokM6UmsaXKF>KwzS z+e<^@SjOgn0+R}5uW4X-Qvh2aM&|gKK~Xnrv|!U2N*cPd0_lDUeK) zWZ2K%M%yQ| zclq|?L}Dl*wPIB$S3p|HC{B9uZ!{8)bC@Jeu<92fico_`6ksQ?cuR?@`&PvH0XTMA z8>)T{rCgvs(1Grvg@ju*Q9BsYj5tr*zY>ckNBgizRLZDrXVaWOPZ;uaXQ&iLeIm$D+ym41nfx)*`uxl6N63J}O`RgO@*SK`-Y}F3p)rqe zo0$7VsbFLR@>i&8U`kYkVI<#^cg;%-PYuN#N@0Jp_l*h4ON0v&c0+e{d)Ll3KT~`3 z59u$GE}3=Zs|ae0k^X*}zQ7EseHpVQ=7d!0cHj^9>RFu=gSEf;y050hRET!~JFJ)X z+&zBea)~%{CTp`bD9rO3Q7bx%#w$)**dl8`Eb56U=nV)0KbyAv659}mJ3nK1M&=^U z(Yf=uiQ;g@&#<>YEYGhT8{DEIMeTaCwomM!;ghNR{wrp6!^Y!kklwj|1I}SLji7)P z){w@h$Jomup{A&vNa=caN|emPQJRnhKgt6A=Jc(8O0@oGiRjI!ii0N#G!4%ej8c;6 z5t@5hZUr~EpCt=A5Ao9l=HDdPH)Bd(hdX`r={kDKN?ELE=bXoG$7_VAtZ>eUTVKqW zM?slTGE|J+j6A^=Q>Vo37z*eTqx>~$paai z%B8WQ_>#qb23xvZg5PVcVD=sPr-PV9EU3(Qn(_0h(^^w{OrBcxGx+I*=ayy+&nS6f zeT|YkKBtB~U4&A)WR;t~k!YMMA1o>|#D74N+HtZcBIl7GbY7T$cFp$$zt_}`xq7RM z+B)Ti%kR^nzGo7Hx|Rh}L=i+`kDt@J!yBO#2 zHj`&=5nFdjQ(sK2%nTr5x$YpIn^SWoB`BWNuo%0z)VYV-YhDrwoJIE7?naM4J@NVX z7FIXWWFjCyy3)E=btBZm%~x4O?LbNOVVZp3g_tridzV0KuG#$VKYG8~=#jEu5Bqe& zqEDsI=A&6-vu$Gw-`71%TGcxSCQjgp0>~qa?6kjc?j96E8s~>UPaRW5uD`Z>*D?7k zlb!UDX1Laq!*Ovlc$#>j{o~{D(@y^j5&vKPrj{i!Z;8480VHBTUsJzxC9%n{^+|01^W$l=Nb9mQk?;Uu;}R8mcp| ztZTgfB|kkb^)V`x^RRqkQgvc(bJ?Y(D}|2`&9cb9-xp1)X>AjkIUbRa+IA7PgmU-i z_-TyuA?k`zfP?i&_CX6ECCV_u@1=EQ$LZ9d1)d1L=_e^|e^_hAHg+41cLUKI`F5Q8 z_~G2#adIi!ysVH2Zi+HQf(^=ctBAlSgX_nO1y`~aA|aWZ6WeSqk@N!=`Kg)*WFhXG zVo>2?v5F)rnp`00E*jL-{B~RO=Zptn_is8WPWABj-FeN| zWQyeBBsWA(S5QKQr276RQ2qaLSt)4^e{p>ek#YJ$#yz*1M_jVy6=PXle0TwdLu`_n|v(#^rav{FeK% zF%g^eHnLu7CATItgf!k;ZiYDCCo33|9JVx zj$?|E*)@uYDI>|kY}(06Em5A;Jy6p)rOs&QU$A4I ztV1^9Erpyw;SqZNvmpQU>}Ssjc&J1+Dyq`@{wmqanM+l8tdu#kj41D1?6kdHL3*GXh&xc~Y0xa(VHhcXvCZCn|%_K)1^&U)+ zw+!icjRfRMQxA>SqIfrM__+E7jLgl1W!2x9zD47~iq7;~IS>Vvns!E=v-wCqmc||% zC2u{$b%Zm8siq6T+OwHGO2B`{+9C~9PHh{ZzQ8lvw}N@2l@9D^p@a;o?T%!=zMS$I zO+&aY!AE$CRGi{`%!vK*~4NR-UTz5hG{fGpdYHUw|hn z1MP&+zPIWJNu5X(YQ<|Jch>D=z~y4fm%!%``O{g}K;!vKV?oInSZeaiDz}ED8t(F$ z$mMWBctjmF=`CpBV@% z_F`>#GkD)9JM*a}T<$tmlD8iUW=ZoFkT)@PA6P+}4Ee9Ja4C~in6b)j^=U6W3I%^t zgL^fh+6PKE#JK6In&1=1KA*CN@AO6#!gW}iDqqc&{tluR*TJJZBgKg%>5?u~`ComAjx@6Xc6i!u<5) za!FKVpMFYM;ww^0Ed+XkKpn%l$o0lbVo~=Q@>^xoLjF;pa7vh%0p8CY*B7yCr;>KU z)>hWAk8PIikvQPEaYV^ukZtL7y`dz*m2+w(&PCu&g;s&C`1mg zsdd)!z$<>(S;EE+EEMFC)K7zsuB-};@T^CarI&AMmy1LT{Y0th{n(^FOrKjf_7CSI zv{@7dxBNtnhffm!)G9U{z$RU}$M%z?UYz(uv9?JD>2Z2 zej+{5mSZ#_m(5KbG!BRMvkj}v#q{BiD*dkgyi_{+v!JW0&8Gt*L0Dqv0Rj@MmAs+U zDjbj}?n2&CoC`~1iE|x$LBamXMR`eTf$5;cb~kiDu&jU>)1XHkHb%j)6)>ys2QGr_ z=7N|?Z62>buF{5rD$+X*0Jc3XDtaGnfA)DC+XWmsS&=#Q>LYO&RC|J9RHzrD63Un7 zB(rs30geMn!fs>0rI^j37FbHRwiDucb(p|d+C!44s!275i=f}{tSQlv&^Th%#_@W&-!)5#qrve0Ih zG*rHvTrYDP7={iydPr*rBv0<_-gJ_OxD^pC+KlEld>mS)@5xpR zj0S(>k4)awki?|ARFFK`QwL0fzVl=`X@*zMRYask?MjFATFquxyril7k@^6kB*p_x zG}X)P;8DesuQv4Ajw%NpuV)lz&YT(-CFd__g(hnSU-%i9@MkTBdM8I-EoN3M$JCz~ zq~+J-m8Wx4?>)G4eU0RHvoKu|{o<-djr=Bye1s%~Ug)h)@YvCVz28Wy4=?A# zy16ikoS&mB4d&w-`O)(7)jHVuT50OF8=B*i@KoSprZ(nr1IjvIyUV^txO?Dv;Kg42 z=Dm1y2kA&(2{hrZ>?he#O-fx`#%W6h`x#Gqf_?Fi3f@zZ8^UZVngNNvHDAL!#$NYu z&%MKnRDQKj2KoMB;5%PdY5m`i`pi$!#R<0psyDnB*0sHNwluS!`FIUDL^U#h#+!)! z1>W_>@1BW`) zAQc0o8cohE_9cfmnRQz{`Is=OV4Q!w#qdM1r_-+FV`-{@|HHdJezrDaj*^Um&)c4y zyc$34?ynzAes6n-{8R3pkx_z&C;GmGpZ7Pe+$T>*s#yOETWusO|L5 zrX!=u1z+Oix#f-jPF6`|)F|q8D`+$(Epo~PobiDtJhm*e6h^qYvbelXxNK<<1fCEM zbrAESBsuCJuBeSTszR1<6ren;D zGRfMM<#@ws6!SSk-rJ2e_c8GzttL6g!S9j+-L^Do?sFSn;lSKSl;*mxKW819tb6nE zfdLgD?J15i{+AulOg6Q83tU_7YfnD(yR}wzl{YKP5?Bx4d0Ee2Thmf ztTET|?geS-@!^jedxWxFL60u-uHY!BN!$`3aPd2z*T?w?iviB@hhXo`-OwXx>D_%f z&`TWSj1hZnx{R17DDNjK7bV7w=c2X_@~5jxQP+mUIn-I+KfKfhrl!f_b9Hb^kAT(~ zKb1mr#WBuv5p%%msm+u3gSHUWpu6Wb3{Uc(7g2++@)Sl!THjaMycm4_ZVV5A)JEtL zSJp8oRS>o?jR&7qJNM&lUL~x*0A(>C@|6;t5UPd?R8j20spn zO0s}bQ+|fxygC5QUxI2C%_41uKIcXddD&AL#o^n6V|nJ(s6A+8;w(SR4;w??hrU@S z0v&uz8byEoN)JmK7w8*#=JjI=1hL&PUElD^w-UTO;=u9G{2mdQ>TTAX(s|tpupD%i zpDu>!k^o;Y?C;y)yU7^!GVl&J;Im=N9VD9qtu1B52^2wjG|Es-cEa^YZgfQRXIe^_Hgic{ifA=Opy$1bC7!|3!`&W&&pse*6-RK4 zzRux=((S@fmlyfn8aQ>lWF-jSwa4U2wkmMAU>U`~ydbQOY5nGid>SWN8sZ6Q$hLQo zax60TCy!R}?J>mO57pZ4yy4A76o4`-_wf+QHnYBLhew)OBjKEo(5!>tTn%^EyG;`D zi2?iZi~|>e#V^!D`waEa{+TgRgQF6%smuGlF4bMCerXEYxZtf&p-#hS=6C)q?~GZ! zu&B-<)ah)x1@pKG#W(veYw&p&b0>Y<26{zx2>C#a*S_} z9Qe6w3Q}XqS6hz@pIFqma~QmdFRU?**^u^7Nv=u>bN31g2+kuX(Z*L{%=uss@mIEr zy|{;BAP34i42IgN?c&XBs|j-Oo4RviRM9$aR708x$l1*En}tl_7|(z6mZ>BIFIkSAQyI&wjC_X%2;zQ63AZsa8-_|h=&s`u++T~SCM8Eq{t=;G=-1zG zB9D2+O~2@{+OXu1nQb@n^5&ffUcH+p&eq6+w)>>(s9sC^<9ZLqCbPc{#YNs`3M4g> zHlI1y#}#DT2RQL_lwgiD-!4B4r>zV`Ts9))y`-uaY)SGFB(`4f`c%0cIyA@97rg-c zCbJ{7z4mMOy9S1^n~!%X%5|!M&4G!?#1Uqc``zsO6$~7-hIW%k6m49;(&e%eIj}Iiy zU|U70)ZXpNe)Is(i_>RCr~?kTvlq|r+RO^Wcj=>`V6;!DKg*aaax<{T5^F$HKGEU= z(8nmVq`3Sg79eZz22fi%A=K* zf23e#5puCDXnJb%l}S@XIeQUmMUFsj7j$wDeNL3vZ(go)tQ6RNKS`D6rt%o0qg~-@ z9YF4YW$`7j12rLl%DT2_RkAg3D(c(nJtN%nE1b(l1-%R0(IHefZm%K&`Eci9c$odbl=KEt;bWN=9=5#0+Y zO0H)hA@OB2xe4tUrHDPB5wZw_CI$x^x!bEC!U2-4`_%a!Fn7*C=mQy}ftes516AJ- zcu~&EqPtNay_cJuVB^_tbA{AAtR|aWmI04n#fyO?$IWqh`*D zweoG?C_Awg&1o&hriPJc)>R+@u?}FtM=c!tDR5%l4Wq|shrW7>DMgX{2ccJwZ`@aa zx!!HAHsqXQQOac4PkXRTTO)Yj_E0GJecRt;^7KCu%0GlNO1UCtWv_3^dJ-%oQWI^` zC!DbfeK7VTb@8QRuzQB5@F5I&?Sq#jS4SxWB@iFf3LjA-+e)%)Cs3y*%M(~b=5>-$ zg^xWcQY8XMjvkf?D@W|vsqM2ruR+2tSh_KvEUu)3HoZ(5T?{GMoOEfxp^bK%`on6q zeYMrwNfGhEov}a>DeB7fB)58x_n5t@LLPvxH_t%9otIMZ$OJBM*r0dNZa47Xl)8{l zO+d4d6TLSqG!sMLUS8lKo&-X28TN4o=v}ezR9WDUe#PUZTj$5s6Kweb&r9dtNQ|KT zDR{WV(!phwMe;xW#QfhRS;{Qpl^7CoF=>8A!u6!O=j^yw7o= zOe6$i^Fv*z&5fIkx+DRaVTOtz|IxyKY4Y9wWLy7(*N2#(Js?VCAT?-C;s3u93;(xV z%6~6;lCfnmdQK)^kh*Wq;P`hg`s+ypB|ofY$7;gC3JgFJ&3dWxF7kQ$&xtV9A zsW*4V?1DBM>NYk}Khk*gzUH$i8T}K-N!NI@>~#0>Qo|zi-NE6NcTvZoyMjg-88lR% zoIPAF5LpEh+b>p zLBVOMK0ucw5|FznU%yD&YHf8e3T6BXHhxm*Y%eIpHl366euOGBVfy!02= zXyL{uw_;r<=Hw8CN{k{ydO)tHiqp*goO23tHlnUxJMs`5pN-gKvxe&%d=?ybTignh zyHSGBBp)Gn+=p?<=xB0M-@#fCCay28o=-I$)SOe05_sNuH2% zW(azm*@9-4AsTqwhq{9CSdDv_$p@%i5Z9F_iX}u*a=J6O3t=rpsce63sX`Z^zsIgn z9lU^?zi$U+PzmMbVPLfQ!R94c7W<4W@X&AebF=l4gLCKhIAn!Nd97uNQXfz9`|%T9 zGeQ1~-#LfT#Pv#JXme6re=G0Ik&O+TA~TJ@ioIYrf*jj zxIzJT2w9Wqz6zInF(C2{P!}rS*{3ZJqTI35;J98BuGFN3XSvE3U}-?Bgh%!qi=zPPE4>jy~84YC9O$xEX6Ra7oSBjc$-fVrM3{pazv$!|a@lhFl@)d7bPKTD9^;?KR+m{A)ZLRoH=2L-V zVyrnKmPMTtEecp`;HyXA%j^k~X$2nDpjr1Kkv`c&iTk2rFNB^xAWXAR#NBwsDAg8a z`!5D{`A9Plm-c5pv;EOcJtocxh;YnZ?o+uag7Y zcH)DCA$Sm7;l!VHy*+PQx;AMWoM~WiC`2150ZngNj18#B{57%>ax=TV zOlb#1=LngV^4yLM5Xj9H3i)d*jJT61{c@CDK!3!Hd<17G)q-X1mexnGepmF!X*=cR zRZd52HV6Azr(PU%bh{uF|)T;_H8Cv%>UXiG3u6@HFqt zbN(*GWc_EU)nS_9yB3dAVY0yh18{{_Ky0HmPd|y|-gFT7waz82R`BNRv56o{?${M&#|hqr_S`opCGed`Zx;mo6JyQJ2+ z0+kURmIFR%BQCK(3z6ExkYy=JQ0uH=PS{G6dN+4W%zn`nxs%k1!mP`#86mJO`TcZD zV6Zfu6WAlh3l^hz9z0x4mPMw8GC7?@ojsap)Htqu^$mRL7J)v+IeJbrfiDA}ic!GB zh^;#+#CJ*akNZ5N(nggp+FXD2($a12PSY@0{; zSCvv6-?<{;OwIE^36@tE%?}ZWQZ^GH6(k3T?(R^Ke~#L_&%%}EMM00hm!sMsH#r9Q z9P9>gzW%+dr%wRABuM?-fs8qYo!u$HJiA}aLlJ(;<@T&E6lAX?P)^eI8Yg`7HUgzX zDvijn&*{?uqaz;aErmrq-ZeOtp9clXtYLN0hQ^oe5T3fq*elJ;Eq(H$CsLcao4N63 zbpyCr#au$Anko$*Yp^kxe04Af&sliGe%RuQB~q@i9(bXHuBWK z%8m8owFTyhIMo0dYFAW%rr(QDVOH0|9Np1e;D9!M_ipKOsb`!YyKaLqb?|O$gf@x5 z{sFqfCrpOM`0%;zrZ3KSJ`+Pe<$%{G?(u7YIb6xVPio1W+!4XI1n``OZW8jGf5a4N z-6Eia?dQ*E7Y##@sYHs$`WnMaz3}wKazZP2t2lKz_IXM1QPSYDr1*G$YOKLJ;4GZa?inbTWloZ}mAHarJryLhsNnpFD;~W8w(p1kw90}Ok52YO5!&pN!1%16Mjs{6Z?7ssvH3XZl1q6aFSV3R0A#ISON>##%V zwa@V7-SAv|{PKA$c(mT+Zoee;bqH2DTA`Dt-6FH`6izj2qQ}GYrlRrsNjak9R5wHB z*VRqaJJM{R#t|w?of>;OsN}8ns{Mfk1)~mL&%3eqLxCQ+!_kr0l?;Bko~ScY!l;?U z#Dw15u>oJhCKr3|Zb%<|shA43D1BR?6HX1hVJ)EvmT*)UoIK@oz^g&EzsMR~g?N$( zUJ)s|ahH+x^Zw)9c+pdoE9sW!-FjLHH;ZDy*vS2 z(coP6ZlORmDXDr?ugkIAg>0Kx4#I3>o9z=Hws$_LOU)nwoJ;!sIcD=`;3H zv9+C{57C9Uj0SCJ+nW&AFmnnYQtMY@F9zoIYnOK(-%^#F*xGh4LzuOf_jchaGwxNw zVh~M=nOgoTg3=ZDH+|_Q63d47woWj%GtFrqv{vGGAL z>nez3ajJ+vb^7G+7g=y)$SPUtSak=$jX}2{jAMzE)TTz6uNb?x7-aoZC{t5$VsJbu z*2_z@&m5_=dtrVcXs*`S_RrN`;A7ZAo@78${8sohhn;K^U`Y(>(bm#Om1&CNwKK>y zPSLGim}<0v@)L8oh4(~CSNLTzHhFV6{K&YDF!j=OhXMWZm!+lR1}qshA6GoeyE8;g z>4(;jvVg_Q%5opVmjqdL3QD=b!Zb;G8#OmMRlpi@71C+`T(62jg30Ejbzl@BU*xpc zzq?_%RRdS72(FjgaBr+y`sW`Bi?%BTvQ5E`+m_b`jz9oa`yk>aM=ZWTzrA^(uC8ak zFWrrw?o3bHD7wNOgbidw%11`%HD^5B#AakhBW4RWRbe>}}d9i?+^x z9!T;SpCZxlm@B(dCzNOP+e7NU`?rVm!rX>HzH^Eu8okDTWugNZJOK}{zuebWH3oGj zlz@>U;_><@=++UU;ps?vh#FgfZ`#!gt)Yc;!zDBf-WJa z-i#n@A(nBBIL&zWFro}5AG@Pb8VKi;=w3ar#TNc#Y}5C((DWc~vl2{km69W&YBb>% z*#oVkuauo2bG0-M>4 zY|7vuR*;5eeG%VxJ1$`u7jHmdt_Ls4=uL7=k^=h4lWEf8#A>z#KIyse1#8Ds%cu8G zE3LbOqp|1?0boaOPgo%qouz?~-v9z~dai6nM1>aT1)b!5j^kzL5_NGUgI`rtH-P)@ zhmIZvq9<<=;O~BCP|LymB28%Zg#6OX-}og_$S+Nmr3u%KaNQfv+Two&oHc?Zj5YB% z23uUVpJpeM1~SKTm=)m*#t&pqq$VuKCy9OKPX+=u3I1tz?q_DbTGh`J@j3e&V``A0 zYEAIdCA-MQdi;;}FS>5eymGaA2KM{LJgy+Hr5Rp5Yno(h#i*A$OxvgSw|A-7=gC-o zA~{`Z2P_Hr9Qn@uQJ#9kpYJ0^3$MtN+PJ5sr&R106^}hHT{f2W?8GaV)yN)wk<8^@ zKs>t;=;9r}DS)bV^~A@H3f{`$y7qX~3W6B_!7vql_*!~85+bJ178$@|45@e+g<7c2 zSR|8}+uaj@M?W2B<~Eos$Pz;HwLVmWv=+f`Tu{e3w?LpurULrkv8L3EJ_CWFWEmwF zTS?KvysgB3UNV#c{PZeCRN3?_FaRwdPl!}aVb%3;b=PTI59qsT1u2YZpS~`Aa+G_L z2Qs!R3QQ4N!{8af0d|ixQ{{wBe96-1+T5gv%8Sj!%#zB2eZV;G@{p$`{NyN)Blw!* z`-C%Xx&&A629i+Tc}KT{!0RE=>|yoF00`c|HC)8Ep6CFr8X(Gqz4y1AQ}nl-)2ARP z&l2=5ceM=av+;Z>W@*%6MyhV&G^JsDOQ@Zy_okuXph4<~=q+OD za^E%`E~LIZ*xK(Dw2NyV-7k#hBg{2uIWIK<_d{0j+KV2np0V~9l07BMt`fz4V$#b( z2H>uxF8JI=hU))y&hG|W_Q`N{^4~Bh3VN77TgHDg^EV1g=28cwplF)%qj{oe6_V)%G|xD# zq^5z5&&AFNAhyHsAQ@*JrH;F+J0@_nFg4Oy9i5%8Kb{2M4F|mRl{xFCJ)^h|XSpRz4uvcn6Wz zJker6%JgY9IQP2mgGX{yE@;U1X5PHxA7E_aj99OXwc2$b^iFws+NkxlXhXqgl>iLd z&M88cf~<8)>*_0gHg)0?;o@w^b9=y2=Rs*<2PvqwE8Jl`i}4PfT+CLVmsujU%jr_+Nct z^Tc1$)W~C&ZR5Rh7i^K*K8CvD{cBiiV5_#}O_6JL7|Qc;vEdaewLce^ z?H8sUD%7ZULyi_*KQ|{rNzTTT8R}zht{g|i~H`T~e6zLBQ>;Kxp zx9k}y3K)J0g|9v9;Q_>V=qt5~DY!_~nc6o$Ewr>kmNr8N2Hi8f^bGoOY~q&Ll3erX zD*m1Z#zc*?@_)!R6P{~P(~6a$u(u#hXYVnH{Jq>`L1f!OwWc`p0{wE)`y)(0yPFV_ zg1680fiFSS{MY^wwSnL2k#qF7VK%&&4X!|j{RXbTE_+5_kr9u;@Q}VlJPipAt*d6FDImcie%AhkOm*_fp z&ie5Pm{cVQP8CNQ7x*Jk((1CYgt6JXzUA+X6nv*Q;mei;vo>9gX*^g z%Fx{x2Nad__I1~JM1q2Mi}y3ap3EPs5cqZf=-sCNzlaE_bmiDZ@iT8n46Z}Um0oH7 z?D^D>-T!cCa@9q}zo>Hqul|;U?3F&%Dx|2DvJSm*7i3*+p2#m0WJSKa?KNZZy}L$< zw*hK2x5j|Vy2dnfF7NRn^YHd9w-`F2{;o6&u%$6gTR-YG!)Hm!t}WKxa`0*6OXfwc zv4rX=zkyyZ{qr~Hw>?*=r~Lhs^SntmFb!RgH0`7#yyRd|#B-UE zZ<};t^XLBMO#A%GNZ|wdae2hzua64Saqo<2u(6r%zTcEM)vxmN%%I5NXvPmw=JAIm zk-}0tB|C7W@8*1ubmN*e&*iNJ=v9ubWs97mlClZ{RG_F6uVo8T66thW zr@@a)Nbcyo<;)+y6tPtt%Z{_<4}WnQE_7IEe|Az&=^w@rJP+FpEt3DAs3iXu>vCrkfsh+2w|cUk^1&ZudhBo3yUT&dE)0 zb9M#i&%OT1GV@JWRTW{{#GEjxYPq?73SKD1AD6UQCN8cl;w6d0>K$qnr#4jIjw>r? zENmMsoxj!WT&H;j9B8VF2v1mu+UO)FE!=Zmc_#=S_-a!9dJ+Hhn!CaXH|+g4OCx$` zT)wtz4)4n?)Dn+93^v2|o3*K;Hf<*E@0VI^k4>%|geJj99$JC)TW#v-8OKfRDTkHc zEK`A#m4rX7H$xXE*3WCSTX@HX!OxIr0S2JGD7q3GUI|~dtyOTtf3zD!r4fP_97rsk zfF4M|W=4%w&h1G6JM`h*OjKjD1ibZfbdDbqjVqQ1@&w0uS%PEeajcWT;__FXKX1nR zaw{Zk26yuoV@zpp{w$ugCsdZztuvCf!*7r5!%@>?xbRLD^nMI1Adg379L(<`vI}o8 zSTVek6@uh-k<*9|99=xv)Y}FIPzjD&>S`hLT(P z{ETmEO8BW|xc!oDv2gtut@~KEe+6qlM0FHdu9ev~;75vqz(W|K1sJzm-WP)1wuoam z9eR!TZGlT`0P_4qndO$gW?bPF!D%P=r47|EXa=V(B}nrzTMQger`b4w8$YPM4fh@a zmsg0KDNe_HwqQG0HQiZak5rR>DoqT@GW~c<@5!FisdKQKCPHlv9~*-qaxa8z75sIY{s=zmL$YPf^_ zc`53WjI~^at51+hDJ5lE_i07R)boz<97wcmZ<(RD(@l&{T>*BH5X;&fXocG02MQ90 zB&eF7*P_*-l(+J52P|6#Eu72gy3=7}`rHU3KM$# zh(nreNk1``fiUfXuNwP_I`!^He2OaSx0DPyM0O-H4|PkOf$a?Xf62kP&oTE^*xTI2Y!*5RdV5( zHyNeE_0is5$sn>8N~4jZrkWx+i7t$>3&fq80X{;ksY(dC{0_3VJ!Q~06$iZRhQ~qh z0u1SxmB=K+?e^*X-|UUG6wI+`9tyl8h(gE=yTr9a3iqdS+6Qv_cioP zEPVN3uJ+~hS`pSY2+6CWciM4>?Qao+oP$@%6N21AS1IE%mm%+QL9QoO1t9eYHeL_; zc#S6O*ZF-JHKt?gha^-5e0)T)SWgwk3I^}>_>q)<`S(+pFIPu&6j1wtH$ zCY*wV!8vP1;=4QT4TyEOSl3vv2*uu&(;rT=Qo(i?&E%ggWd(B+?q-^5A={d9@srrk z7a!m1wgHAMQ1ii4`6k6W%p3^CEM;4*f9<*mh46vlO59Bru8tMFW<@6VnE-Ofun`oD zWHvJ@$%iv4)wc27*p>5NQJ7SnL|p99*kGio+8uv6C6X@(uXzxGE7y^v)rc`SQu%?x ztSe6>l8$UjT0t_~Cl7w-Vc$e8j)4dqyYno3QUBJ_ekj}pW+#aUD23mkTTS<~B=Vy?lz#kZLcoj9iM;{wqYYHy- z0wb@8%w&cN>aatN)$?qrf5z1GBf(H*jl*s1dJ-{$*OL4T3h{fU?eQ1=kmf>j6;!uS zOmxACHEO;Az3@qx4V$-fQ@dbgCxRFC!6)cRC*lzw;%GrW-dbH~jUDKO3r8^5tK4d_QueGObCOKm*#su4$`bZnH$z4SLjr_2 zylXCG);>~Z;3=0x==w3i0^r$3w=k2&(CVkdy#arI%gS4nkXsO&#V2vzG1rGUp zy}`Cs@r@Su9Lpw5yZG)n@^&h;XJ20oq-Vv5)?Z}qQr|0m`GrG=C)4}M(q&V8KDE?4 zgj5VZ7?noo+ktB{E;l@|Fh`@@9yt}xpD$fG<4c|0sPiq42`d`bAMr`Adm_h-t)>sa z-rfrI>cX3uhFLF3=BJIgDohqc+Uyz@gz;z`n^y_i@||EX8Jp^ zdu5A*u;cEznSXh8|4;2OweW`HgggGKq~s-S2G_ql2X{qHe{0!EeWfLVfNLE)-wD>n zY!RAqcC2mBjU73ET5Z5S2b69x`|!&AyK=Ih!9K0j5KlXVGm7H~;xhLxrCNQ7|EBA- r{fo%ve@7quzqpd!Kgn(|uU= 0 { + panic("struct embeds multiple Object types") + } + baseIdx = i + } + if baseIdx < 0 { + panic("struct does not embed an Object") + } + + // add Component fields as children + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if i == baseIdx { + continue + } + + if !field.IsExported() { + continue + } + + // all uninitialized fields are ignored since they cant contain valid component references + value := v.Field(i) + if value.IsZero() { + continue + } + + // the field contains a reference to an instantiated component + // if its an orphan, add it to the object's children + if child, ok := value.Interface().(Component); ok { + if child.Parent() == nil { + Attach(obj, child) + } + } + } + + return obj +} + +func (g *object) Transform() transform.T { + return g.transform +} + +func (o *object) setParent(parent Object) { + // check for cycles + ancestor := parent + for ancestor != nil { + if ancestor.ID() == o.ID() { + panic("cyclical object hierarchies are not allowed") + } + ancestor = ancestor.Parent() + } + + o.component.setParent(parent) + if parent != nil { + o.transform.SetParent(parent.Transform()) + } else { + o.transform.SetParent(nil) + } +} + +func (g *object) Update(scene Component, dt float32) { + for _, child := range g.children { + if child.Enabled() { + child.Update(scene, dt) + } + } +} + +func (g *object) Children() []Component { + return g.children +} + +func (g *object) attach(children ...Component) { + for _, child := range children { + g.attachIfNotChild(child) + } +} + +func (g *object) attachIfNotChild(child Component) { + for _, existing := range g.children { + if existing.ID() == child.ID() { + return + } + } + g.children = append(g.children, child) +} + +func (g *object) detach(child Component) { + for i, existing := range g.children { + if existing.ID() == child.ID() { + g.children = append(g.children[:i], g.children[i+1:]...) + return + } + } +} + +func (g *object) setActive(active bool) bool { + wasActive := g.component.setActive(active) + if active { + for _, child := range g.children { + activate(child) + } + } else { + for _, child := range g.children { + deactivate(child) + } + } + return wasActive +} + +func (g *object) KeyEvent(e keys.Event) { + for _, child := range g.children { + if !child.Enabled() { + continue + } + if handler, ok := child.(input.KeyHandler); ok { + handler.KeyEvent(e) + if e.Handled() { + return + } + } + } +} + +func (g *object) MouseEvent(e mouse.Event) { + for _, child := range g.children { + if !child.Enabled() { + continue + } + if handler, ok := child.(input.MouseHandler); ok { + handler.MouseEvent(e) + if e.Handled() { + return + } + } + } +} + +func (o *object) Destroy() { + // iterate over a copy of the child slice, since it will be mutated + // when the child detaches itself during destruction + children := make([]Component, len(o.Children())) + copy(children, o.Children()[:]) + + for _, child := range o.Children() { + child.Destroy() + } + + if o.parent != nil { + o.parent.detach(o) + } +} diff --git a/engine/object/builder.go b/engine/object/builder.go new file mode 100644 index 0000000..bb4d4c1 --- /dev/null +++ b/engine/object/builder.go @@ -0,0 +1,99 @@ +package object + +import ( + "zworld/engine/renderapi/texture" + "zworld/plugins/math/quat" + "zworld/plugins/math/vec3" +) + +// Builder API for game objects +type builder[K Object] struct { + object K + + position vec3.T + rotation quat.T + scale vec3.T + active bool + + parent Object + children []Component +} + +// Builder instantiates a new group builder. +func Builder[K Object](object K) *builder[K] { + return &builder[K]{ + object: object, + position: vec3.Zero, + rotation: quat.Ident(), + scale: vec3.One, + active: true, + } +} + +// Attach a child component +func (b *builder[K]) Attach(child Component) *builder[K] { + b.children = append(b.children, child) + return b +} + +// Set the parent of the object +func (b *builder[K]) Parent(parent Object) *builder[K] { + b.parent = parent + return b +} + +// Position sets the intial position of the object. +func (b *builder[K]) Position(p vec3.T) *builder[K] { + b.position = p + return b +} + +// Rotation sets the intial rotation of the object. +func (b *builder[K]) Rotation(r quat.T) *builder[K] { + b.rotation = r + return b +} + +// Scale sets the intial scale of the object. +func (b *builder[K]) Scale(s vec3.T) *builder[K] { + b.scale = s + return b +} + +// Active sets the objects active flag. +func (b *builder[K]) Active(active bool) *builder[K] { + b.active = active + return b +} + +func (b *builder[K]) Texture(slot texture.Slot, ref texture.Ref) *builder[K] { + type Textured interface { + SetTexture(slot texture.Slot, ref texture.Ref) + } + if textured, ok := any(b.object).(Textured); ok { + textured.SetTexture(slot, ref) + } else { + // todo: raise a warning if its not possible? + } + return b +} + +// Create instantiates a new object with the current builder settings. +func (b *builder[K]) Create() K { + obj := b.object + obj.Transform().SetPosition(b.position) + obj.Transform().SetRotation(b.rotation) + obj.Transform().SetScale(b.scale) + if b.active { + Enable(obj) + } else { + Disable(obj) + } + for _, child := range b.children { + Attach(obj, child) + } + if b.parent != nil { + Attach(b.parent, obj) + } + return obj +} diff --git a/engine/object/camera/camera.go b/engine/object/camera/camera.go new file mode 100644 index 0000000..884880d --- /dev/null +++ b/engine/object/camera/camera.go @@ -0,0 +1,102 @@ +package camera + +import ( + "zworld/engine/object" + "zworld/engine/renderapi" + "zworld/engine/renderapi/color" + "zworld/plugins/math/mat4" + "zworld/plugins/math/vec3" +) + +// Camera Group +type Object struct { + object.Object + *Camera +} + +// Camera Component +type Camera struct { + object.Component + Args + + Viewport renderapi.Screen + Aspect float32 + Proj mat4.T + View mat4.T + ViewInv mat4.T + ViewProj mat4.T + ViewProjInv mat4.T + Eye vec3.T + Forward vec3.T +} + +type Args struct { + Fov float32 + Near float32 + Far float32 + Clear color.T +} + +// New creates a new camera component. +func New(args Args) *Camera { + return object.NewComponent(&Camera{ + Args: args, + Aspect: 1, + }) +} + +func NewObject(args Args) *Object { + return object.New("Camera", &Object{ + Camera: New(args), + }) +} + +func (cam *Object) Name() string { return "Camera" } + +// Unproject screen space coordinates into world space +func (cam *Camera) Unproject(pos vec3.T) vec3.T { + // screen space -> clip space + pos.Y = 1 - pos.Y + pos = pos.Scaled(2).Sub(vec3.One) + + // unproject to world space by multiplying inverse view-projection + return cam.ViewProjInv.TransformPoint(pos) +} + +func (cam *Camera) RenderArgs(screen renderapi.Screen) renderapi.Args { + // todo: passing the global viewport allows the camera to modify the actual render viewport + + // update view & view-projection matrices + cam.Viewport = screen + cam.Aspect = float32(cam.Viewport.Width) / float32(cam.Viewport.Height) + cam.Proj = mat4.Perspective(cam.Fov, cam.Aspect, cam.Near, cam.Far) + + // calculate the view matrix. + // should be the inverse of the cameras transform matrix + tf := cam.Transform() + + cam.Eye = tf.WorldPosition() + cam.Forward = tf.Forward() + + cam.ViewInv = tf.Matrix() + cam.View = cam.ViewInv.Invert() + + cam.ViewProj = cam.Proj.Mul(&cam.View) + cam.ViewProjInv = cam.ViewProj.Invert() + + return renderapi.Args{ + Viewport: cam.Viewport, + Near: cam.Near, + Far: cam.Far, + Fov: cam.Fov, + Projection: cam.Proj, + View: cam.View, + ViewInv: cam.ViewInv, + VP: cam.ViewProj, + VPInv: cam.ViewProjInv, + MVP: cam.ViewProj, + Clear: cam.Clear, + Position: cam.Transform().WorldPosition(), + Forward: cam.Transform().Forward(), + } +} diff --git a/engine/object/camera/frustum.go b/engine/object/camera/frustum.go new file mode 100644 index 0000000..4e647ae --- /dev/null +++ b/engine/object/camera/frustum.go @@ -0,0 +1,57 @@ +package camera + +import ( + "zworld/plugins/math" + "zworld/plugins/math/mat4" + "zworld/plugins/math/vec3" +) + +type Frustum struct { + Corners vec3.Array + Center vec3.T + Min vec3.T + Max vec3.T +} + +var ndc_corners = vec3.Array{ + vec3.New(-1, 1, -1), // NTL + vec3.New(1, 1, -1), // NTR + vec3.New(-1, -1, -1), // NBL + vec3.New(1, -1, -1), // NBR + vec3.New(-1, 1, 1), // FTL + vec3.New(1, 1, 1), // FTR + vec3.New(-1, -1, 1), // FBL + vec3.New(1, -1, 1), // FBR +} + +// NewFrustum creates a view frustum from an inverse view projection matrix by unprojecting the corners of the NDC cube. +func NewFrustum(vpi mat4.T) Frustum { + return Frustum{ + Corners: ndc_corners, + Center: vec3.Zero, + Min: vec3.New(-1, -1, -1), + Max: vec3.One, + }.Transform(vpi) +} + +// Transform returns a new frustum with all its vertices transformed by the given matrix +func (f Frustum) Transform(transform mat4.T) Frustum { + corners := make(vec3.Array, 8) + center := vec3.Zero + min := vec3.New(math.InfPos, math.InfPos, math.InfPos) + max := vec3.New(math.InfNeg, math.InfNeg, math.InfNeg) + for i, corner := range f.Corners { + corner = transform.TransformPoint(corner) + center = center.Add(corner) + min = vec3.Min(min, corner) + max = vec3.Max(max, corner) + corners[i] = corner + } + center = center.Scaled(1 / 8.0) + return Frustum{ + Corners: corners, + Center: center, + Min: min, + Max: max, + } +} diff --git a/engine/object/component.go b/engine/object/component.go new file mode 100644 index 0000000..c85dd54 --- /dev/null +++ b/engine/object/component.go @@ -0,0 +1,141 @@ +package object + +import ( + "reflect" + "zworld/plugins/math/transform" +) + +type Component interface { + // ID returns a unique identifier for this object. + ID() uint + + // Name is used to identify the object within the scene. + Name() string + + // Parent returns the parent of this object, or nil + Parent() Object + + // Transform returns the object transform + Transform() transform.T + + // Active indicates whether the object is active in the scene or not. + // E.g. the object/component and all its parents are enabled and active. + Active() bool + + // Enabled indicates whether the object is currently enabled or not. + // Note that the object can still be inactive if an ancestor is disabled. + Enabled() bool + + // Update the object. Called on every frame. + Update(Component, float32) + + // Destroy the object + Destroy() + + setName(string) + setParent(Object) + setEnabled(bool) bool + setActive(bool) bool +} + +type component struct { + id uint + name string + enabled bool + active bool + parent Object +} + +func emptyComponent(name string) component { + return component{ + id: ID(), + name: name, + enabled: true, + active: false, + } +} + +// componentType caches a reference to Component's reflect.Type +var componentType = reflect.TypeOf((*Component)(nil)).Elem() + +func NewComponent[K Component](cmp K) K { + t := reflect.TypeOf(cmp).Elem() + v := reflect.ValueOf(cmp).Elem() + + // find & initialize base component + baseIdx := -1 + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if !field.Anonymous { + // only anonymous fields are considered + continue + } + if !field.IsExported() { + // only exported fields can be base fields + continue + } + + value := v.Field(i) + if field.Type == componentType { + // the components directly extends the base component + // if its nil, create a new empty component base + if value.IsZero() { + base := emptyComponent(t.Name()) + value.Set(reflect.ValueOf(&base)) + } + } else if _, isComponent := value.Interface().(Component); isComponent { + // this object extends some other non-base object + } else { + // its not an object, move on + continue + } + + baseIdx = i + } + if baseIdx < 0 { + panic("struct does not embed a Component") + } + + return cmp +} + +func (b *component) ID() uint { + return b.id +} + +func (b *component) Update(scene Component, dt float32) { +} + +func (b *component) Transform() transform.T { + if b.parent == nil { + return transform.Identity() + } + return b.parent.Transform() +} + +func (b *component) Active() bool { return b.active } +func (b *component) setActive(active bool) bool { + prev := b.active + b.active = active + return prev +} + +func (b *component) Enabled() bool { return b.enabled } +func (b *component) setEnabled(enabled bool) bool { + prev := b.enabled + b.enabled = enabled + return prev +} + +func (b *component) Parent() Object { return b.parent } +func (b *component) setParent(p Object) { b.parent = p } + +func (b *component) setName(n string) { b.name = n } +func (b *component) Name() string { return b.name } +func (b *component) String() string { return b.Name() } + +func (o *component) Destroy() { + if o.parent != nil { + o.parent.detach(o) + } +} diff --git a/engine/object/light/directional.go b/engine/object/light/directional.go new file mode 100644 index 0000000..62779fc --- /dev/null +++ b/engine/object/light/directional.go @@ -0,0 +1,236 @@ +package light + +import ( + "zworld/engine/object" + "zworld/engine/render/uniform" + "zworld/engine/renderapi" + "zworld/engine/renderapi/color" + "zworld/plugins/math" + "zworld/plugins/math/mat4" + "zworld/plugins/math/vec3" + "zworld/plugins/math/vec4" +) + +type DirectionalArgs struct { + Color color.T + Intensity float32 + Shadows bool + Cascades int +} + +type Cascade struct { + View mat4.T + Proj mat4.T + ViewProj mat4.T + NearSplit float32 + FarSplit float32 +} + +type Directional struct { + object.Component + cascades []Cascade + + Color object.Property[color.T] + Intensity object.Property[float32] + Shadows object.Property[bool] + + CascadeLambda object.Property[float32] + CascadeBlend object.Property[float32] +} + +var _ T = &Directional{} + +func init() { + object.Register[*Directional](DeserializeDirectional) +} + +func NewDirectional(args DirectionalArgs) *Directional { + lit := object.NewComponent(&Directional{ + cascades: make([]Cascade, args.Cascades), + + Color: object.NewProperty(args.Color), + Intensity: object.NewProperty(args.Intensity), + Shadows: object.NewProperty(args.Shadows), + + CascadeLambda: object.NewProperty[float32](0.9), + CascadeBlend: object.NewProperty[float32](3.0), + }) + return lit +} + +func (lit *Directional) Name() string { return "DirectionalLight" } +func (lit *Directional) Type() Type { return TypeDirectional } +func (lit *Directional) CastShadows() bool { return lit.Shadows.Get() } + +func farSplitDist(cascade, cascades int, near, far, splitLambda float32) float32 { + clipRange := far - near + minZ := near + maxZ := near + clipRange + + rnge := maxZ - minZ + ratio := maxZ / minZ + + // Calculate split depths based on view camera frustum + // Based on method presented in https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch10.html + p := (float32(cascade) + 1) / float32(cascades) + log := minZ * math.Pow(ratio, p) + uniform := minZ + rnge*p + d := splitLambda*(log-uniform) + uniform + return (d - near) / clipRange +} + +func nearSplitDist(cascade, cascades int, near, far, splitLambda float32) float32 { + if cascade == 0 { + return 0 + } + return farSplitDist(cascade-1, cascades, near, far, splitLambda) +} + +func (lit *Directional) PreDraw(args renderapi.Args, scene object.Object) error { + // update cascades + for i, _ := range lit.cascades { + lit.cascades[i] = lit.calculateCascade(args, i, len(lit.cascades)) + } + return nil +} + +func (lit *Directional) calculateCascade(args renderapi.Args, cascade, cascades int) Cascade { + texSize := float32(2048) + + frustumCorners := []vec3.T{ + vec3.New(-1, 1, -1), // NTL + vec3.New(1, 1, -1), // NTR + vec3.New(-1, -1, -1), // NBL + vec3.New(1, -1, -1), // NBR + vec3.New(-1, 1, 1), // FTL + vec3.New(1, 1, 1), // FTR + vec3.New(-1, -1, 1), // FBL + vec3.New(1, -1, 1), // FBR + } + + // transform frustum into world space + for i, corner := range frustumCorners { + frustumCorners[i] = args.VPInv.TransformPoint(corner) + } + + // squash + nearSplit := nearSplitDist(cascade, cascades, args.Near, args.Far, lit.CascadeLambda.Get()) + farSplit := farSplitDist(cascade, cascades, args.Near, args.Far, lit.CascadeLambda.Get()) + for i := 0; i < 4; i++ { + dist := frustumCorners[i+4].Sub(frustumCorners[i]) + frustumCorners[i] = frustumCorners[i].Add(dist.Scaled(nearSplit)) + frustumCorners[i+4] = frustumCorners[i].Add(dist.Scaled(farSplit)) + } + + // calculate frustum center + center := vec3.Zero + for _, corner := range frustumCorners { + center = center.Add(corner) + } + center = center.Scaled(float32(1) / 8) + + radius := float32(0) + for _, corner := range frustumCorners { + distance := vec3.Distance(corner, center) + radius = math.Max(radius, distance) + } + radius = math.Snap(radius, 16) + + // create light view matrix looking at the center of the + // camera frustum + ldir := lit.Transform().Forward() + position := center.Sub(ldir.Scaled(radius)) + lview := mat4.LookAt(position, center, vec3.UnitY) + + lproj := mat4.Orthographic( + -radius-0.01, radius+0.01, + -radius-0.01, radius+0.01, + 0, 2*radius) + + lvp := lproj.Mul(&lview) + + // round the center of the lights projection to the nearest texel + origin := lvp.TransformPoint(vec3.New(0, 0, 0)).Scaled(texSize / 2.0) + offset := origin.Round().Sub(origin) + offset.Scale(2.0 / texSize) + lproj[12] = offset.X + lproj[13] = offset.Y + + // re-create view-projection after rounding + lvp = lproj.Mul(&lview) + + return Cascade{ + Proj: lproj, + View: lview, + ViewProj: lvp, + NearSplit: nearSplit * args.Far, + FarSplit: farSplit * args.Far, + } +} + +func (lit *Directional) LightData(shadowmaps ShadowmapStore) uniform.Light { + ldir := lit.Transform().Forward() + entry := uniform.Light{ + Type: uint32(TypeDirectional), + Position: vec4.Extend(ldir, 0), + Color: lit.Color.Get(), + Intensity: lit.Intensity.Get(), + Range: lit.CascadeBlend.Get(), + } + + for cascadeIndex, cascade := range lit.cascades { + entry.ViewProj[cascadeIndex] = cascade.ViewProj + entry.Distance[cascadeIndex] = cascade.FarSplit + if handle, exists := shadowmaps.Lookup(lit, cascadeIndex); exists { + entry.Shadowmap[cascadeIndex] = uint32(handle) + } + } + + return entry +} + +func (lit *Directional) Shadowmaps() int { + return len(lit.cascades) +} + +func (lit *Directional) ShadowProjection(mapIndex int) uniform.Camera { + cascade := lit.cascades[mapIndex] + return uniform.Camera{ + Proj: cascade.Proj, + View: cascade.View, + ViewProj: cascade.ViewProj, + ProjInv: cascade.Proj.Invert(), + ViewInv: cascade.View.Invert(), + ViewProjInv: cascade.ViewProj.Invert(), + Eye: vec4.Extend(lit.Transform().Position(), 0), + Forward: vec4.Extend(lit.Transform().Forward(), 0), + } +} + +type DirectionalState struct { + object.ComponentState + DirectionalArgs +} + +func (lit *Directional) Serialize(enc object.Encoder) error { + return enc.Encode(DirectionalState{ + // send help + ComponentState: object.NewComponentState(lit.Component), + DirectionalArgs: DirectionalArgs{ + Color: lit.Color.Get(), + Intensity: lit.Intensity.Get(), + Shadows: lit.Shadows.Get(), + }, + }) +} + +func DeserializeDirectional(dec object.Decoder) (object.Component, error) { + var state DirectionalState + if err := dec.Decode(&state); err != nil { + return nil, err + } + + obj := NewDirectional(state.DirectionalArgs) + obj.Component = state.ComponentState.New() + return obj, nil +} diff --git a/engine/object/light/light.go b/engine/object/light/light.go new file mode 100644 index 0000000..842f46c --- /dev/null +++ b/engine/object/light/light.go @@ -0,0 +1,37 @@ +package light + +import ( + "zworld/engine/object" + "zworld/engine/render/uniform" + "zworld/engine/renderapi/color" + "zworld/plugins/math/mat4" + "zworld/plugins/math/vec4" +) + +type ShadowmapStore interface { + Lookup(T, int) (int, bool) +} + +type T interface { + object.Component + + Type() Type + CastShadows() bool + Shadowmaps() int + LightData(ShadowmapStore) uniform.Light + ShadowProjection(mapIndex int) uniform.Camera +} + +// Descriptor holds rendering information for lights +type Descriptor struct { + Projection mat4.T // Light projection matrix + View mat4.T // Light view matrix + ViewProj mat4.T + Color color.T + Position vec4.T + Type Type + Range float32 + Intensity float32 + Shadows uint32 + Index int +} diff --git a/engine/object/light/point.go b/engine/object/light/point.go new file mode 100644 index 0000000..8f9a05c --- /dev/null +++ b/engine/object/light/point.go @@ -0,0 +1,89 @@ +package light + +import ( + "zworld/engine/object" + "zworld/engine/render/uniform" + "zworld/engine/renderapi/color" + "zworld/plugins/math/vec4" +) + +type PointArgs struct { + Color color.T + Range float32 + Intensity float32 +} + +type Point struct { + object.Component + + Color object.Property[color.T] + Range object.Property[float32] + Intensity object.Property[float32] + Falloff object.Property[float32] +} + +var _ T = &Point{} + +func init() { + object.Register[*Point](DeserializePoint) +} + +func NewPoint(args PointArgs) *Point { + return object.NewComponent(&Point{ + Color: object.NewProperty(args.Color), + Range: object.NewProperty(args.Range), + Intensity: object.NewProperty(args.Intensity), + Falloff: object.NewProperty(float32(2)), + }) +} + +func (lit *Point) Name() string { return "PointLight" } +func (lit *Point) Type() Type { return TypePoint } +func (lit *Point) CastShadows() bool { return false } + +func (lit *Point) LightData(shadowmaps ShadowmapStore) uniform.Light { + return uniform.Light{ + Type: uint32(TypePoint), + Position: vec4.Extend(lit.Transform().WorldPosition(), 0), + Color: lit.Color.Get(), + Intensity: lit.Intensity.Get(), + Range: lit.Range.Get(), + Falloff: lit.Falloff.Get(), + } +} + +func (lit *Point) Shadowmaps() int { + return 0 +} + +func (lit *Point) ShadowProjection(mapIndex int) uniform.Camera { + panic("todo") +} + +type PointState struct { + object.ComponentState + PointArgs +} + +func (lit *Point) Serialize(enc object.Encoder) error { + return enc.Encode(PointState{ + // send help + ComponentState: object.NewComponentState(lit.Component), + PointArgs: PointArgs{ + Color: lit.Color.Get(), + Intensity: lit.Intensity.Get(), + Range: lit.Range.Get(), + }, + }) +} + +func DeserializePoint(dec object.Decoder) (object.Component, error) { + var state PointState + if err := dec.Decode(&state); err != nil { + return nil, err + } + + obj := NewPoint(state.PointArgs) + obj.Component = state.ComponentState.New() + return obj, nil +} diff --git a/engine/object/light/type.go b/engine/object/light/type.go new file mode 100644 index 0000000..1d00325 --- /dev/null +++ b/engine/object/light/type.go @@ -0,0 +1,12 @@ +package light + +// Type indicates which kind of light. Point, Directional etc +type Type uint32 + +const ( + // PointLight is a normal light casting rays in all directions. + TypePoint Type = 1 + + // DirectionalLight is a directional light source, casting parallell rays. + TypeDirectional Type = 2 +) diff --git a/engine/object/mesh/dynamic.go b/engine/object/mesh/dynamic.go new file mode 100644 index 0000000..5c5bb55 --- /dev/null +++ b/engine/object/mesh/dynamic.go @@ -0,0 +1,67 @@ +package mesh + +import ( + "log" + "zworld/engine/object" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/vertex" +) + +type Generator[V vertex.Vertex, I vertex.Index] func() Data[V, I] + +type Data[V vertex.Vertex, I vertex.Index] struct { + Vertices []V + Indices []I +} + +type Dynamic[V vertex.Vertex, I vertex.Index] struct { + *Static + + name string + refresh Generator[V, I] + updated chan Data[V, I] + meshdata vertex.MutableMesh[V, I] +} + +func NewDynamic[V vertex.Vertex, I vertex.Index](name string, mat *material.Def, fn Generator[V, I]) *Dynamic[V, I] { + m := &Dynamic[V, I]{ + Static: New(mat), + name: name, + refresh: fn, + updated: make(chan Data[V, I], 2), + } + m.meshdata = vertex.NewTriangles(object.Key(name, m), []V{}, []I{}) + m.VertexData.Set(m.meshdata) + m.RefreshSync() + + return m +} + +func (m *Dynamic[V, I]) Name() string { + return m.name +} + +func (m *Dynamic[V, I]) Refresh() { + log.Println("mesh", m, ": async refresh") + go func() { + data := m.refresh() + m.updated <- data + }() +} + +func (m *Dynamic[V, I]) RefreshSync() { + log.Println("mesh", m, ": blocking refresh") + data := m.refresh() + m.meshdata.Update(data.Vertices, data.Indices) + m.VertexData.Set(m.meshdata) +} + +func (m *Dynamic[V, I]) Update(scene object.Component, dt float32) { + m.Static.Update(scene, dt) + select { + case data := <-m.updated: + m.meshdata.Update(data.Vertices, data.Indices) + m.VertexData.Set(m.meshdata) + default: + } +} diff --git a/engine/object/mesh/mesh.go b/engine/object/mesh/mesh.go new file mode 100644 index 0000000..0cb6ea8 --- /dev/null +++ b/engine/object/mesh/mesh.go @@ -0,0 +1,149 @@ +package mesh + +import ( + "zworld/engine/object" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/texture" + "zworld/engine/renderapi/vertex" + "zworld/plugins/math/shape" + "zworld/plugins/math/vec3" +) + +func init() { + object.Register[*Static](Deserialize) +} + +type Mesh interface { + object.Component + + Primitive() vertex.Primitive + CastShadows() bool + Material() *material.Def + MaterialID() material.ID + + Texture(texture.Slot) texture.Ref + + // Bounding sphere used for view frustum culling + BoundingSphere() shape.Sphere + + // returns the VertexData property + // this is kinda ugly - but other components might need to subscribe to changes ? + Mesh() *object.Property[vertex.Mesh] +} + +// mesh base +type Static struct { + object.Component + + primitive vertex.Primitive + shadows bool + mat *material.Def + matId material.ID + + textures map[texture.Slot]texture.Ref + + // bounding radius + center vec3.T + radius float32 + + VertexData object.Property[vertex.Mesh] +} + +// New creates a new mesh component +func New(mat *material.Def) *Static { + return NewPrimitiveMesh(vertex.Triangles, mat) +} + +// NewLines creates a new line mesh component +func NewLines() *Static { + return NewPrimitiveMesh(vertex.Lines, nil) +} + +// NewPrimitiveMesh creates a new mesh composed of a given GL primitive +func NewPrimitiveMesh(primitive vertex.Primitive, mat *material.Def) *Static { + m := object.NewComponent(&Static{ + mat: mat, + matId: material.Hash(mat), + textures: make(map[texture.Slot]texture.Ref), + primitive: primitive, + shadows: true, + + VertexData: object.NewProperty[vertex.Mesh](nil), + }) + m.VertexData.OnChange.Subscribe(func(data vertex.Mesh) { + // refresh bounding sphere + min := data.Min() + max := data.Max() + m.center = max.Sub(min).Scaled(0.5) + m.radius = m.center.Length() + }) + return m +} + +//func (m *Static) Name() string { +//return "Mesh" +//} + +func (m *Static) Primitive() vertex.Primitive { return m.primitive } +func (m *Static) Mesh() *object.Property[vertex.Mesh] { return &m.VertexData } + +func (m *Static) Texture(slot texture.Slot) texture.Ref { + return m.textures[slot] +} + +func (m *Static) SetTexture(slot texture.Slot, ref texture.Ref) { + m.textures[slot] = ref +} + +func (m *Static) CastShadows() bool { + return m.primitive == vertex.Triangles && m.shadows && !m.mat.Transparent +} + +func (m *Static) SetShadows(shadows bool) { + m.shadows = shadows +} + +func (m *Static) Material() *material.Def { + return m.mat +} + +func (m *Static) MaterialID() material.ID { + return m.matId +} + +func (m *Static) BoundingSphere() shape.Sphere { + return shape.Sphere{ + Center: m.Transform().WorldPosition().Add(m.center), + Radius: m.radius, + } +} + +type MeshState struct { + object.ComponentState + Primitive vertex.Primitive + Material material.Def +} + +func (m *Static) State() MeshState { + return MeshState{ + // send help + ComponentState: object.NewComponentState(m.Component), + Primitive: m.primitive, + Material: *m.Material(), + } +} + +func (m *Static) Serialize(enc object.Encoder) error { + return enc.Encode(m.State()) +} + +func Deserialize(dec object.Decoder) (object.Component, error) { + var state MeshState + if err := dec.Decode(&state); err != nil { + return nil, err + } + + obj := NewPrimitiveMesh(state.Primitive, &state.Material) + obj.Component = state.ComponentState.New() + return obj, nil +} diff --git a/engine/object/property.go b/engine/object/property.go new file mode 100644 index 0000000..4b6c65b --- /dev/null +++ b/engine/object/property.go @@ -0,0 +1,100 @@ +package object + +import ( + "fmt" + "reflect" + "zworld/plugins/system/events" +) + +type PropValue interface{} + +type GenericProp interface { + Type() reflect.Type + GetAny() any + SetAny(any) +} + +type Property[T PropValue] struct { + value T + def T + kind reflect.Type + + OnChange events.Event[T] +} + +var _ GenericProp = &Property[int]{} + +func NewProperty[T PropValue](def T) Property[T] { + var empty T + return Property[T]{ + value: def, + def: def, + kind: reflect.TypeOf(empty), + } +} + +func (p *Property[T]) Get() T { + return p.value +} + +func (p *Property[T]) GetAny() any { + return p.value +} + +func (p *Property[T]) Set(value T) { + p.value = value + p.OnChange.Emit(value) +} + +func (p *Property[T]) SetAny(value any) { + if cast, ok := value.(T); ok { + p.Set(cast) + } +} + +func (p *Property[T]) String() string { + return fmt.Sprintf("%v", p.value) +} + +func (p *Property[T]) Type() reflect.Type { + return p.kind +} + +type PropInfo struct { + GenericProp + Key string + Name string +} + +func Properties(target Component) []PropInfo { + t := reflect.TypeOf(target).Elem() + v := reflect.ValueOf(target).Elem() + + properties := make([]PropInfo, 0, t.NumField()) + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if field.Anonymous { + // anonymous fields are not considered + continue + } + if !field.IsExported() { + // only exported fields can be properties + continue + } + + value := v.Field(i) + + if prop, isProp := value.Addr().Interface().(GenericProp); isProp { + // todo: tags + + properties = append(properties, PropInfo{ + GenericProp: prop, + + Key: field.Name, + Name: field.Name, + }) + } + } + + return properties +} diff --git a/engine/object/query.go b/engine/object/query.go new file mode 100644 index 0000000..9503728 --- /dev/null +++ b/engine/object/query.go @@ -0,0 +1,119 @@ +package object + +import ( + "sort" + "zworld/engine/util" +) + +type Query[K Component] struct { + results []K + filters []func(b K) bool + sorter func(a, b K) bool +} + +// NewQuery returns a new query for the given component type +func NewQuery[K Component]() *Query[K] { + return &Query[K]{ + filters: make([]func(K) bool, 0, 8), + results: make([]K, 0, 128), + } +} + +// Where applies a filter predicate to the results +func (q *Query[K]) Where(predicate func(K) bool) *Query[K] { + q.filters = append(q.filters, predicate) + return q +} + +// Sort the result using a compare function. +// The compare function should return true if a is "less than" b +func (q *Query[K]) Sort(sorter func(a, b K) bool) *Query[K] { + q.sorter = sorter + return q +} + +// Match returns true if the passed component matches the query +func (q *Query[K]) match(component K) bool { + for _, filter := range q.filters { + if !filter(component) { + return false + } + } + return true +} + +// Append a component to the query results. +func (q *Query[K]) append(result K) { + q.results = append(q.results, result) +} + +// Clear the query results, without freeing the memory. +func (q *Query[K]) Reset() *Query[K] { + // clear slice, but keep the memory + q.results = q.results[:0] + q.filters = q.filters[:0] + return q +} + +// First returns the first match in a depth-first fashion +func (q *Query[K]) First(root Component) (K, bool) { + result, hit := q.first(root) + return result, hit +} + +func (q *Query[K]) first(root Component) (K, bool) { + var empty K + if !root.Enabled() { + return empty, false + } + if k, ok := root.(K); ok { + if q.match(k) { + return k, true + } + } + if group, ok := root.(Object); ok { + for _, child := range group.Children() { + if match, found := q.first(child); found { + return match, true + } + } + } + return empty, false +} + +// Collect returns all matching components +func (q *Query[K]) Collect(roots ...Component) []K { + // collect all matches + for _, root := range roots { + q.collect(root) + } + + // sort if required + if q.sorter != nil { + sort.Slice(q.results, func(i, j int) bool { + return q.sorter(q.results[i], q.results[j]) + }) + } + + return q.results +} + +func (q *Query[K]) CollectObjects(roots ...Component) []Component { + return util.Map(NewQuery[K]().Collect(roots...), func(s K) Component { return s }) +} + +func (q *Query[K]) collect(object Component) { + if !object.Enabled() { + return + } + if k, ok := object.(K); ok { + if q.match(k) { + q.append(k) + } + } + if group, ok := object.(Object); ok { + for _, child := range group.Children() { + q.collect(child) + } + } +} diff --git a/engine/object/relations.go b/engine/object/relations.go new file mode 100644 index 0000000..3cda140 --- /dev/null +++ b/engine/object/relations.go @@ -0,0 +1,300 @@ +package object + +// Returns all the children of an object. Returns the empty slice if the object is a component. +func Children(object Component) []Component { + if group, ok := object.(Object); ok { + return group.Children() + } + return nil +} + +// Returns the child objects attached to an object +func Subgroups(object Component) []Object { + children := Children(object) + groups := make([]Object, 0, len(children)) + for _, child := range children { + if group, ok := child.(Object); ok { + groups = append(groups, group) + } + } + return groups +} + +// Returns the components attached to an object +func Components(object Component) []Component { + children := Children(object) + components := make([]Component, 0, len(children)) + for _, child := range children { + _, group := child.(Object) + if !group { + components = append(components, child) + } + } + return components +} + +// Attach a child component/object to a parent object +// If the object already has a parent, it will be detached first. +func Attach(parent Object, child Component) { + if child == nil { + panic("attaching nil child") + } + Detach(child) + child.setParent(parent) + parent.attach(child) + activate(child) +} + +// Detach a child component/object from its parent object +// Does nothing if the given object has no parent. +func Detach(child Component) { + if child.Parent() == nil { + return + } + deactivate(child) + child.Parent().detach(child) + child.setParent(nil) +} + +func Enable(object Component) { + object.setEnabled(true) + activate(object) +} + +func activate(object Component) { + if !object.Enabled() { + return + } + if object.Parent() == nil || !object.Parent().Active() { + return + } + // activate if parent is active + if wasActive := object.setActive(true); !wasActive { + // enabled + if handler, ok := object.(EnableHandler); ok { + handler.OnEnable() + } + } +} + +func Disable(object Component) { + object.setEnabled(false) + deactivate(object) +} + +func deactivate(object Component) { + if wasActive := object.setActive(false); wasActive { + // disabled + if handler, ok := object.(DisableHandler); ok { + handler.OnDisable() + } + } +} + +func Toggle(object Component, enabled bool) { + if enabled { + Enable(object) + } else { + Disable(object) + } +} + +// Root returns the first ancestor of the given component/object +func Root(obj Component) Component { + for obj.Parent() != nil { + obj = obj.Parent() + } + return obj +} + +// Gets a reference to a component of type K on the same object as the component/object specified. +func Get[K Component](self Component) K { + if hit, ok := self.(K); ok { + return hit + } + var empty K + group, ok := self.(Object) + if !ok { + group = self.Parent() + } + if group == nil { + return empty + } + if !group.Enabled() { + return empty + } + for _, child := range group.Children() { + if child == self { + continue + } + if !child.Enabled() { + continue + } + if hit, ok := child.(K); ok { + return hit + } + } + return empty +} + +// Gets references to all components of type K on the same object as the component/object specified. +func GetAll[K Component](self Component) []K { + group, ok := self.(Object) + if !ok { + group = self.Parent() + } + if group == nil { + return nil + } + if !group.Enabled() { + return nil + } + var results []K + if hit, ok := group.(K); ok { + results = append(results, hit) + } + for _, child := range group.Children() { + if !child.Enabled() { + continue + } + if hit, ok := child.(K); ok { + results = append(results, hit) + } + } + return results +} + +// Gets the first reference to a component of type K in any parent of the object/component. +// For component targets, sibling components will be returned. +func GetInParents[K Component](self Component) K { + var empty K + group := self.Parent() + for group != nil { + if !group.Enabled() { + return empty + } + if hit, ok := group.(K); ok { + return hit + } + for _, child := range group.Children() { + if child == self { + continue + } + if !child.Enabled() { + continue + } + if hit, ok := child.(K); ok { + return hit + } + } + group = group.Parent() + } + + return empty +} + +// Gets references to all components of type K in any parent of the object/component. +// For component targets, sibling components will be returned. +func GetAllInParents[K Component](self Component) []K { + group := self.Parent() + var results []K + for group != nil { + if !group.Enabled() { + return nil + } + if hit, ok := group.(K); ok { + results = append(results, hit) + } + for _, child := range group.Children() { + if child == self { + continue + } + if !child.Enabled() { + continue + } + if hit, ok := child.(K); ok { + results = append(results, hit) + } + } + group = group.Parent() + } + return results +} + +// Gets a reference to a component of type K on the same object as the component/object specified, or any child of the object. +func GetInChildren[K Component](self Component) K { + var empty K + group, ok := self.(Object) + if !ok { + group = self.Parent() + } + if group == nil { + return empty + } + if !group.Enabled() { + return empty + } + + todo := []Object{group} + + for len(todo) > 0 { + group = todo[0] + todo = todo[1:] + + for _, child := range group.Children() { + if child == self { + continue + } + if !child.Enabled() { + continue + } + if hit, ok := child.(K); ok { + return hit + } + if childgroup, ok := child.(Object); ok { + todo = append(todo, childgroup) + } + } + } + + return empty +} + +// Gets references to all components of type K on the same object as the component/object specified, or any child of the object. +func GetAllInChildren[K Component](self Component) []K { + group, ok := self.(Object) + if !ok { + group = self.Parent() + } + if group == nil { + return nil + } + if !group.Enabled() { + return nil + } + + todo := []Object{group} + var results []K + + for len(todo) > 0 { + group = todo[0] + todo = todo[1:] + + for _, child := range group.Children() { + if child == self { + continue + } + if !child.Enabled() { + continue + } + if hit, ok := child.(K); ok { + results = append(results, hit) + } + if childgroup, ok := child.(Object); ok { + todo = append(todo, childgroup) + } + } + } + + return results +} diff --git a/engine/object/serialize.go b/engine/object/serialize.go new file mode 100644 index 0000000..f285109 --- /dev/null +++ b/engine/object/serialize.go @@ -0,0 +1,195 @@ +package object + +import ( + "encoding/gob" + "errors" + "fmt" + "io" + "reflect" + "zworld/plugins/math/quat" + "zworld/plugins/math/vec3" +) + +type Decoder interface { + Decode(e any) error +} + +type Encoder interface { + Encode(data any) error +} + +type Serializable interface { + Serialize(Encoder) error +} + +type MemorySerializer struct { + stream []any + index int +} + +func (m *MemorySerializer) Encode(data any) error { + m.stream = append(m.stream, data) + return nil +} + +func (m *MemorySerializer) Decode(target any) error { + if m.index >= len(m.stream) { + return io.EOF + } + reflect.ValueOf(target).Elem().Set(reflect.ValueOf(m.stream[m.index])) + m.index++ + return nil +} + +func Copy(obj Component) Component { + buffer := &MemorySerializer{} + + err := Serialize(buffer, obj) + if err != nil { + panic(err) + } + + kopy, err := Deserialize(buffer) + if err != nil { + panic(err) + } + + return kopy +} + +func Save(writer io.Writer, obj Component) error { + enc := gob.NewEncoder(writer) + return Serialize(enc, obj) +} + +func Load(reader io.Reader) (Component, error) { + dec := gob.NewDecoder(reader) + return Deserialize(dec) +} + +type ComponentState struct { + ID uint + Name string + Enabled bool +} + +func NewComponentState(c Component) ComponentState { + return ComponentState{ + ID: c.ID(), + Name: c.Name(), + Enabled: c.Enabled(), + } +} + +func (c ComponentState) New() Component { + return &component{ + id: c.ID, + name: c.Name, + enabled: c.Enabled, + } +} + +type ObjectState struct { + ComponentState + Position vec3.T + Rotation quat.T + Scale vec3.T + Children int +} + +type DeserializeFn func(Decoder) (Component, error) + +var ErrSerialize = errors.New("serialization error") + +var types = map[string]DeserializeFn{} + +func typeName(obj any) string { + t := reflect.TypeOf(obj).Elem() + return t.PkgPath() + "/" + t.Name() +} + +func init() { + Register[*object](DeserializeObject) +} + +func Register[T Serializable](deserializer DeserializeFn) { + var empty T + kind := typeName(empty) + types[kind] = deserializer +} + +func Serialize(enc Encoder, obj Component) error { + kind := typeName(obj) + serializable, ok := obj.(Serializable) + if !ok { + return fmt.Errorf("%w: %s is not serializable", ErrSerialize, kind) + } + if err := enc.Encode(kind); err != nil { + return err + } + return serializable.Serialize(enc) +} + +func Deserialize(decoder Decoder) (Component, error) { + var kind string + if err := decoder.Decode(&kind); err != nil { + return nil, err + } + deserializer, exists := types[kind] + if !exists { + return nil, fmt.Errorf("%w: no deserializer for %s", ErrSerialize, kind) + } + return deserializer(decoder) +} + +func (o *object) Serialize(enc Encoder) error { + children := 0 + for _, child := range o.children { + if _, ok := child.(Serializable); ok { + children++ + } + } + + if err := enc.Encode(ObjectState{ + ComponentState: NewComponentState(o), + Position: o.transform.Position(), + Rotation: o.transform.Rotation(), + Scale: o.transform.Scale(), + Children: children, + }); err != nil { + return err + } + + // serialize children + for _, child := range o.children { + if err := Serialize(enc, child); err != nil { + if errors.Is(err, ErrSerialize) { + continue + } + return err + } + } + return nil +} + +func DeserializeObject(dec Decoder) (Component, error) { + var data ObjectState + if err := dec.Decode(&data); err != nil { + return nil, err + } + obj := Empty(data.Name) + obj.setEnabled(data.Enabled) + obj.Transform().SetPosition(data.Position) + obj.Transform().SetRotation(data.Rotation) + obj.Transform().SetScale(data.Scale) + + // deserialize children + for i := 0; i < data.Children; i++ { + child, err := Deserialize(dec) + if err != nil { + return nil, err + } + Attach(obj, child) + } + return obj, nil +} diff --git a/engine/object/type.go b/engine/object/type.go new file mode 100644 index 0000000..67f10a9 --- /dev/null +++ b/engine/object/type.go @@ -0,0 +1,11 @@ +package object + +type EnableHandler interface { + Component + OnEnable() +} + +type DisableHandler interface { + Component + OnDisable() +} diff --git a/engine/object/utils.go b/engine/object/utils.go new file mode 100644 index 0000000..1611c14 --- /dev/null +++ b/engine/object/utils.go @@ -0,0 +1,19 @@ +package object + +import ( + "math/rand" + "strconv" +) + +func Key(prefix string, object Component) string { + p := len(prefix) + buffer := make([]byte, p+1, p+9) + copy(buffer, []byte(prefix)) + buffer[p] = '-' + dst := strconv.AppendUint(buffer, uint64(object.ID()), 16) + return string(dst) +} + +func ID() uint { + return uint(rand.Int63n(0xFFFFFFFF)) +} diff --git a/engine/profiling.go b/engine/profiling.go new file mode 100644 index 0000000..8b677a3 --- /dev/null +++ b/engine/profiling.go @@ -0,0 +1,16 @@ +package engine + +import ( + "fmt" + "log" + "net/http" + _ "net/http/pprof" +) + +func RunProfilingServer(port int) { + if err := http.ListenAndServe(fmt.Sprintf("localhost:%d", port), nil); err != nil { + log.Println("failed to launch profiling http server on port", port) + } else { + log.Printf("pprof server available at http://localhost:%d\n", port) + } +} diff --git a/engine/render/graph/default.go b/engine/render/graph/default.go new file mode 100644 index 0000000..3d89cc1 --- /dev/null +++ b/engine/render/graph/default.go @@ -0,0 +1,98 @@ +package graph + +import ( + "github.com/vkngwrapper/core/v2/core1_0" + "zworld/engine/render/pass" + "zworld/engine/renderapi/vulkan" +) + +// Instantiates the default render graph +func Default(app vulkan.App, target vulkan.Target) T { + return New(app, target, func(g T, output vulkan.Target) []Resource { + size := output.Size() + + // + // screen buffers + // + + // allocate main depth buffer + depth := vulkan.NewDepthTarget(app.Device(), "main-depth", size) + + // main off-screen color buffer + hdrBuffer := vulkan.NewColorTarget(app.Device(), "main-color", core1_0.FormatR16G16B16A16SignedFloat, size) + + // create geometry buffer + gbuffer, err := pass.NewGbuffer(app.Device(), size) + if err != nil { + panic(err) + } + + // allocate SSAO output buffer + ssaoFormat := core1_0.FormatR16SignedFloat + ssaoOutput := vulkan.NewColorTarget(app.Device(), "ssao-output", ssaoFormat, vulkan.TargetSize{ + Width: size.Width / 2, + Height: size.Height / 2, + Frames: size.Frames, + Scale: size.Scale, + }) + + // + // main render pass + // + + shadows := pass.NewShadowPass(app, output) + shadowNode := g.Node(shadows) + + // depth pre-pass + depthPass := g.Node(pass.NewDepthPass(app, depth, gbuffer)) + + // deferred geometry + deferredGeometry := g.Node(pass.NewDeferredGeometryPass(app, depth, gbuffer)) + deferredGeometry.After(depthPass, core1_0.PipelineStageTopOfPipe) + + // ssao pass + ssao := g.Node(pass.NewAmbientOcclusionPass(app, ssaoOutput, gbuffer)) + ssao.After(deferredGeometry, core1_0.PipelineStageTopOfPipe) + + // ssao blur pass + blurOutput := vulkan.NewColorTarget(app.Device(), "blur-output", ssaoOutput.SurfaceFormat(), ssaoOutput.Size()) + blur := g.Node(pass.NewBlurPass(app, blurOutput, ssaoOutput)) + blur.After(ssao, core1_0.PipelineStageTopOfPipe) + + // deferred lighting + deferredLighting := g.Node(pass.NewDeferredLightingPass(app, hdrBuffer, gbuffer, shadows, blurOutput)) + deferredLighting.After(shadowNode, core1_0.PipelineStageTopOfPipe) + deferredLighting.After(blur, core1_0.PipelineStageTopOfPipe) + + // forward pass + forward := g.Node(pass.NewForwardPass(app, hdrBuffer, depth, shadows)) + forward.After(deferredLighting, core1_0.PipelineStageTopOfPipe) + + // + // final image composition + // + + // post process pass + composition := vulkan.NewColorTarget(app.Device(), "composition", hdrBuffer.SurfaceFormat(), hdrBuffer.Size()) + post := g.Node(pass.NewPostProcessPass(app, composition, hdrBuffer)) + post.After(forward, core1_0.PipelineStageTopOfPipe) + + lines := g.Node(pass.NewLinePass(app, composition, depth)) + lines.After(post, core1_0.PipelineStageTopOfPipe) + + //gui := g.Node(pass.NewGuiPass(app, composition)) + //gui.After(lines, core1_0.PipelineStageTopOfPipe) + + //outputPass := g.Node(pass.NewOutputPass(app, output, composition)) + //outputPass.After(gui, core1_0.PipelineStageTopOfPipe) + + return []Resource{ + depth, + hdrBuffer, + gbuffer, + ssaoOutput, + blurOutput, + composition, + } + }) +} diff --git a/engine/render/graph/graph.go b/engine/render/graph/graph.go new file mode 100644 index 0000000..7f23e9b --- /dev/null +++ b/engine/render/graph/graph.go @@ -0,0 +1,174 @@ +package graph + +import ( + "fmt" + "github.com/vkngwrapper/core/v2/core1_0" + "image" + "log" + "time" + "zworld/engine/object" + "zworld/engine/renderapi/upload" + "zworld/engine/renderapi/vulkan" +) + +type NodeFunc func(T, vulkan.Target) []Resource + +// The render graph is responsible for synchronization between +// different render nodes. +type T interface { + Node(pass NodePass) Node + Recreate() + Draw(scene object.Object, time, delta float32) + Destroy() + Screengrab() *image.RGBA + Screenshot() +} + +type Resource interface { + Destroy() +} + +type graph struct { + app vulkan.App + target vulkan.Target + pre *preNode + post *postNode + nodes []Node + todo map[Node]bool + init NodeFunc + resources []Resource +} + +func New(app vulkan.App, output vulkan.Target, init NodeFunc) T { + g := &graph{ + app: app, + target: output, + nodes: make([]Node, 0, 16), + todo: make(map[Node]bool, 16), + init: init, + } + g.Recreate() + return g +} + +func (g *graph) Recreate() { + g.Destroy() + g.app.Pool().Recreate() + + g.resources = g.init(g, g.target) + + g.pre = newPreNode(g.app, g.target) + g.post = newPostNode(g.app, g.target) + g.connect() +} + +func (g *graph) Node(pass NodePass) Node { + nd := newNode(g.app, pass.Name(), pass) + g.nodes = append(g.nodes, nd) + return nd +} + +func (g *graph) connect() { + // use bottom of pipe so that subsequent passes start as soon as possible + for _, node := range g.nodes { + if len(node.Requires()) == 0 { + node.After(g.pre, core1_0.PipelineStageTopOfPipe) + } + } + for _, node := range g.nodes { + if len(node.Dependants()) == 0 { + g.post.After(node, core1_0.PipelineStageTopOfPipe) + } + } +} + +func (g *graph) Draw(scene object.Object, time, delta float32) { + // put all nodes in a todo list + // for each node in todo list + // if all Before nodes are not in todo list + // record node + // remove node from todo list + for _, n := range g.nodes { + g.todo[n] = true + } + + ready := func(n Node) bool { + for _, req := range n.Requires() { + if g.todo[req] { + return false + } + } + return true + } + + // prepare + args, context, err := g.pre.Prepare(scene, time, delta) + if err != nil { + log.Println("Render preparation error:", err) + g.Recreate() + return + } + + // select a suitable worker for this frame + worker := g.app.Worker(args.Frame) + + for len(g.todo) > 0 { + progress := false + for node := range g.todo { + // check if ready + if ready(node) { + log.Println(":::draw ", node.Name(), scene.Name()) + node.Draw(worker, *args, scene) + delete(g.todo, node) + progress = true + break + } + } + if !progress { + // dependency error + panic("unable to make progress in render graph") + } + } + + g.post.Present(worker, context) +} + +func (g *graph) Screengrab() *image.RGBA { + idx := 0 + g.app.Device().WaitIdle() + source := g.target.Surfaces()[idx] + ss, err := upload.DownloadImage(g.app.Device(), g.app.Transferer(), source) + if err != nil { + panic(err) + } + return ss +} + +func (g *graph) Screenshot() { + img := g.Screengrab() + filename := fmt.Sprintf("Screenshot-%s.png", time.Now().Format("2006-01-02_15-04-05")) + if err := upload.SavePng(img, filename); err != nil { + panic(err) + } + log.Println("saved screenshot", filename) +} + +func (g *graph) Destroy() { + g.app.Flush() + for _, resource := range g.resources { + resource.Destroy() + } + g.resources = nil + if g.pre != nil { + g.pre.Destroy() + g.pre = nil + } + if g.post != nil { + g.post.Destroy() + g.post = nil + } + for _, node := range g.nodes { + node.Destroy() + } + g.nodes = g.nodes[:0] +} diff --git a/engine/render/graph/node.go b/engine/render/graph/node.go new file mode 100644 index 0000000..4bd1fe0 --- /dev/null +++ b/engine/render/graph/node.go @@ -0,0 +1,185 @@ +package graph + +import ( + "fmt" + "github.com/vkngwrapper/core/v2/core1_0" + "zworld/engine/object" + "zworld/engine/renderapi" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/sync" + "zworld/engine/renderapi/vulkan" +) + +type NodePass interface { + Name() string + Record(command.Recorder, renderapi.Args, object.Component) + Destroy() +} + +type Node interface { + After(nd Node, mask core1_0.PipelineStageFlags) + Before(nd Node, mask core1_0.PipelineStageFlags, signal []sync.Semaphore) + Requires() []Node + Dependants() []Node + + Name() string + Draw(command.Worker, renderapi.Args, object.Component) + Detach(Node) + Destroy() +} + +type node struct { + name string + app vulkan.App + pass NodePass + after map[string]edge + before map[string]edge + requires []Node + dependants []Node +} + +type edge struct { + node Node + mask core1_0.PipelineStageFlags + signal []sync.Semaphore +} + +func newNode(app vulkan.App, name string, pass NodePass) *node { + return &node{ + app: app, + name: name, + pass: pass, + after: make(map[string]edge, 4), + before: make(map[string]edge, 4), + requires: make([]Node, 0, 4), + dependants: make([]Node, 0, 4), + } +} + +func (n *node) Requires() []Node { return n.requires } +func (n *node) Dependants() []Node { return n.dependants } + +func (n *node) After(nd Node, mask core1_0.PipelineStageFlags) { + if _, exists := n.after[nd.Name()]; exists { + return + } + signal := sync.NewSemaphoreArray(n.app.Device(), fmt.Sprintf("%s->%s", nd.Name(), n.name), 3) + n.after[nd.Name()] = edge{ + node: nd, + mask: mask, + signal: signal, + } + nd.Before(n, mask, signal) + + n.refresh() +} + +func (n *node) Before(nd Node, mask core1_0.PipelineStageFlags, signal []sync.Semaphore) { + if _, exists := n.before[nd.Name()]; exists { + return + } + n.before[nd.Name()] = edge{ + node: nd, + mask: mask, + signal: signal, + } + nd.After(n, mask) + + n.refresh() +} + +func (n *node) refresh() { + // recompute signals + n.dependants = make([]Node, 0, len(n.after)) + for _, edge := range n.before { + n.dependants = append(n.dependants, edge.node) + } + + // recompute waits + n.requires = make([]Node, 0, len(n.after)) + for _, edge := range n.after { + if edge.signal == nil { + // skip nil signals + continue + } + n.requires = append(n.requires, edge.node) + } +} + +func (n *node) Detach(nd Node) { + if _, exists := n.before[nd.Name()]; exists { + delete(n.before, nd.Name()) + nd.Detach(n) + } + if edge, exists := n.after[nd.Name()]; exists { + delete(n.after, nd.Name()) + nd.Detach(n) + // free semaphores + for _, signal := range edge.signal { + signal.Destroy() + } + } + n.refresh() +} + +func (n *node) Name() string { + return n.name +} + +func (n *node) Destroy() { + for _, edge := range n.before { + before := edge.node + before.Detach(n) + for _, s := range edge.signal { + s.Destroy() + } + } + for _, edge := range n.after { + after := edge.node + after.Detach(n) + } + if n.pass != nil { + n.pass.Destroy() + n.pass = nil + } + n.before = nil + n.after = nil +} + +func (n *node) waits(index int) []command.Wait { + waits := make([]command.Wait, 0, len(n.after)) + for _, after := range n.after { + if after.signal == nil { + // skip nil signals + continue + } + waits = append(waits, command.Wait{ + Semaphore: after.signal[index], + Mask: after.mask, + }) + } + return waits +} + +func (n *node) signals(index int) []sync.Semaphore { + signals := make([]sync.Semaphore, 0, len(n.before)) + for _, edge := range n.before { + signals = append(signals, edge.signal[index]) + } + return signals +} + +func (n *node) Draw(worker command.Worker, args renderapi.Args, scene object.Component) { + if n.pass == nil { + return + } + cmds := command.NewRecorder() + n.pass.Record(cmds, args, scene) + worker.Queue(cmds.Apply) + + worker.Submit(command.SubmitInfo{ + Marker: n.pass.Name(), + Wait: n.waits(args.Frame), + Signal: n.signals(args.Frame), + }) +} diff --git a/engine/render/graph/post_node.go b/engine/render/graph/post_node.go new file mode 100644 index 0000000..87b1830 --- /dev/null +++ b/engine/render/graph/post_node.go @@ -0,0 +1,44 @@ +package graph + +import ( + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/swapchain" + "zworld/engine/renderapi/sync" + "zworld/engine/renderapi/vulkan" +) + +type postNode struct { + *node + target vulkan.Target +} + +func newPostNode(app vulkan.App, target vulkan.Target) *postNode { + return &postNode{ + node: newNode(app, "Post", nil), + target: target, + } +} + +func (n *postNode) Present(worker command.Worker, context *swapchain.Context) { + var signal []sync.Semaphore + if context.RenderComplete != nil { + signal = []sync.Semaphore{context.RenderComplete} + } + + worker.Submit(command.SubmitInfo{ + Marker: n.Name(), + Wait: n.waits(context.Index), + Signal: signal, + Callback: func() { + context.Release() + }, + }) + + // present + n.target.Present(worker, context) + + // flush ensures all commands are submitted before we start rendering the next frame. otherwise, frame submissions may overlap. + // todo: perhaps its possible to do this at a later stage? e.g. we could run update loop etc while waiting + // note: this is only required if we use multiple/per-frame workers + // worker.Flush() +} diff --git a/engine/render/graph/pre_node.go b/engine/render/graph/pre_node.go new file mode 100644 index 0000000..17e4daf --- /dev/null +++ b/engine/render/graph/pre_node.go @@ -0,0 +1,100 @@ +package graph + +import ( + "errors" + "github.com/vkngwrapper/core/v2/core1_0" + "zworld/engine/object" + "zworld/engine/object/camera" + "zworld/engine/renderapi" + "zworld/engine/renderapi/color" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/swapchain" + "zworld/engine/renderapi/vulkan" + "zworld/plugins/math/mat4" +) + +var ErrRecreate = errors.New("recreate renderer") + +type PreDrawable interface { + object.Component + PreDraw(renderapi.Args, object.Object) error +} + +type preNode struct { + *node + target vulkan.Target + cameraQuery *object.Query[*camera.Camera] + predrawQuery *object.Query[PreDrawable] +} + +func newPreNode(app vulkan.App, target vulkan.Target) *preNode { + return &preNode{ + node: newNode(app, "Pre", nil), + target: target, + cameraQuery: object.NewQuery[*camera.Camera](), + predrawQuery: object.NewQuery[PreDrawable](), + } +} + +func (n *preNode) Prepare(scene object.Object, time, delta float32) (*renderapi.Args, *swapchain.Context, error) { + screen := renderapi.Screen{ + Width: n.target.Width(), + Height: n.target.Height(), + Scale: n.target.Scale(), + } + + // aquire next frame + context, err := n.target.Aquire() + if err != nil { + return nil, nil, ErrRecreate + } + + // ensure the default white texture is always available + n.app.Textures().Fetch(color.White) + + // cache ticks + n.app.Meshes().Tick() + n.app.Textures().Tick() + + // create render arguments + args := renderapi.Args{} + + // find the first active camera + if camera, exists := n.cameraQuery.Reset().First(scene); exists { + args = camera.RenderArgs(screen) + } else { + args.Viewport = screen + } + + // fill in time & swapchain context + args.Frame = context.Index + args.Time = time + args.Delta = delta + args.Transform = mat4.Ident() + + // execute pre-draw pass + objects := n.predrawQuery.Reset().Collect(scene) + for _, object := range objects { + object.PreDraw(args.Apply(object.Transform().Matrix()), scene) + } + + // fire off render start signals + var waits []command.Wait + if context.ImageAvailable != nil { + waits = []command.Wait{ + { + Semaphore: context.ImageAvailable, + Mask: core1_0.PipelineStageColorAttachmentOutput, + }, + } + } + + worker := n.app.Worker(context.Index) + worker.Submit(command.SubmitInfo{ + Marker: n.Name(), + Wait: waits, + Signal: n.signals(context.Index), + }) + + return &args, context, nil +} diff --git a/engine/render/pass/basic_material.go b/engine/render/pass/basic_material.go new file mode 100644 index 0000000..3349ef2 --- /dev/null +++ b/engine/render/pass/basic_material.go @@ -0,0 +1,66 @@ +package pass + +import ( + "zworld/engine/object/light" + "zworld/engine/object/mesh" + "zworld/engine/render/uniform" + "zworld/engine/renderapi/cache" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/material" +) + +type BasicDescriptors struct { + descriptor.Set + Camera *descriptor.Uniform[uniform.Camera] + Objects *descriptor.Storage[uniform.Object] +} + +// Basic Materials only contain camera & object descriptors +// They can be used for various untextured objects, such +// as shadow/depth passes and lines. +type BasicMaterial struct { + Instance *material.Instance[*BasicDescriptors] + Objects *ObjectBuffer + Meshes cache.MeshCache + + id material.ID +} + +func (m *BasicMaterial) ID() material.ID { + return m.id +} + +func (m *BasicMaterial) Begin(camera uniform.Camera, lights []light.T) { + m.Instance.Descriptors().Camera.Set(camera) + m.Objects.Reset() +} + +func (m *BasicMaterial) Bind(cmds command.Recorder) { + cmds.Record(func(cmd command.Buffer) { + m.Instance.Bind(cmd) + }) +} + +func (m *BasicMaterial) End() { + m.Objects.Flush(m.Instance.Descriptors().Objects) +} + +func (m *BasicMaterial) Draw(cmds command.Recorder, msh mesh.Mesh) { + vkmesh, meshReady := m.Meshes.TryFetch(msh.Mesh().Get()) + if !meshReady { + return + } + + index := m.Objects.Store(uniform.Object{ + Model: msh.Transform().Matrix(), + }) + + cmds.Record(func(cmd command.Buffer) { + vkmesh.Draw(cmd, index) + }) +} + +func (m *BasicMaterial) Destroy() { + m.Instance.Material().Destroy() +} diff --git a/engine/render/pass/blur.go b/engine/render/pass/blur.go new file mode 100644 index 0000000..9f90e22 --- /dev/null +++ b/engine/render/pass/blur.go @@ -0,0 +1,127 @@ +package pass + +import ( + "fmt" + "github.com/vkngwrapper/core/v2/core1_0" + "zworld/engine/object" + "zworld/engine/renderapi" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/framebuffer" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/renderpass/attachment" + "zworld/engine/renderapi/shader" + "zworld/engine/renderapi/texture" + "zworld/engine/renderapi/vertex" + "zworld/engine/renderapi/vulkan" +) + +type BlurPass struct { + app vulkan.App + material *material.Material[*BlurDescriptors] + input vulkan.Target + + quad vertex.Mesh + desc []*material.Instance[*BlurDescriptors] + tex []texture.T + fbufs framebuffer.Array + pass renderpass.T +} + +var _ Pass = &BlurPass{} + +type BlurDescriptors struct { + descriptor.Set + Input *descriptor.Sampler +} + +func NewBlurPass(app vulkan.App, output vulkan.Target, input vulkan.Target) *BlurPass { + p := &BlurPass{ + app: app, + input: input, + } + frames := input.Frames() + + p.quad = vertex.ScreenQuad("blur-pass-quad") + + p.pass = renderpass.New(app.Device(), renderpass.Args{ + Name: "Blur", + ColorAttachments: []attachment.Color{ + { + Name: OutputAttachment, + Image: attachment.FromImageArray(output.Surfaces()), + LoadOp: core1_0.AttachmentLoadOpDontCare, + FinalLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + }, + }, + Subpasses: []renderpass.Subpass{ + { + Name: MainSubpass, + ColorAttachments: []attachment.Name{OutputAttachment}, + }, + }, + }) + + p.material = material.New( + app.Device(), + material.Args{ + Shader: app.Shaders().Fetch(shader.NewRef("blur")), + Pass: p.pass, + Pointers: vertex.ParsePointers(vertex.T{}), + DepthTest: false, + DepthWrite: false, + }, + &BlurDescriptors{ + Input: &descriptor.Sampler{ + Stages: core1_0.StageFragment, + }, + }) + + var err error + p.fbufs, err = framebuffer.NewArray(frames, app.Device(), "blur", output.Width(), output.Height(), p.pass) + if err != nil { + panic(err) + } + + p.desc = p.material.InstantiateMany(app.Pool(), frames) + p.tex = make([]texture.T, frames) + for i := range p.tex { + key := fmt.Sprintf("blur-%d", i) + p.tex[i], err = texture.FromImage(app.Device(), key, p.input.Surfaces()[i], texture.Args{ + Filter: texture.FilterNearest, + Wrap: texture.WrapClamp, + }) + if err != nil { + // todo: clean up + panic(err) + } + p.desc[i].Descriptors().Input.Set(p.tex[i]) + } + + return p +} + +func (p *BlurPass) Record(cmds command.Recorder, args renderapi.Args, scene object.Component) { + quad := p.app.Meshes().Fetch(p.quad) + + cmds.Record(func(cmd command.Buffer) { + cmd.CmdBeginRenderPass(p.pass, p.fbufs[args.Frame]) + p.desc[args.Frame].Bind(cmd) + quad.Draw(cmd, 0) + cmd.CmdEndRenderPass() + }) +} + +func (p *BlurPass) Name() string { + return "Blur" +} + +func (p *BlurPass) Destroy() { + for _, tex := range p.tex { + tex.Destroy() + } + p.fbufs.Destroy() + p.pass.Destroy() + p.material.Destroy() +} diff --git a/engine/render/pass/deferred_cache.go b/engine/render/pass/deferred_cache.go new file mode 100644 index 0000000..10e9a4d --- /dev/null +++ b/engine/render/pass/deferred_cache.go @@ -0,0 +1,95 @@ +package pass + +import ( + "github.com/vkngwrapper/core/v2/core1_0" + "zworld/engine/render/uniform" + "zworld/engine/renderapi/cache" + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/shader" + "zworld/engine/renderapi/vertex" + "zworld/engine/renderapi/vulkan" +) + +type DeferredMatCache struct { + app vulkan.App + pass renderpass.T + frames int +} + +func NewDeferredMaterialCache(app vulkan.App, pass renderpass.T, frames int) MaterialCache { + return cache.New[*material.Def, []Material](&DeferredMatCache{ + app: app, + pass: pass, + frames: frames, + }) +} + +func (m *DeferredMatCache) Name() string { return "DeferredMaterials" } + +func (m *DeferredMatCache) Instantiate(def *material.Def, callback func([]Material)) { + if def == nil { + def = material.StandardDeferred() + } + + desc := &DeferredDescriptors{ + Camera: &descriptor.Uniform[uniform.Camera]{ + Stages: core1_0.StageAll, + }, + Objects: &descriptor.Storage[uniform.Object]{ + Stages: core1_0.StageAll, + Size: 2000, + }, + Textures: &descriptor.SamplerArray{ + Stages: core1_0.StageFragment, + Count: 100, + }, + } + + // read vertex pointers from vertex format + pointers := vertex.ParsePointers(def.VertexFormat) + + // fetch shader from cache + shader := m.app.Shaders().Fetch(shader.NewRef(def.Shader)) + + // create material + mat := material.New( + m.app.Device(), + material.Args{ + Shader: shader, + Pass: m.pass, + Subpass: MainSubpass, + Pointers: pointers, + DepthTest: def.DepthTest, + DepthWrite: def.DepthWrite, + DepthClamp: def.DepthClamp, + DepthFunc: def.DepthFunc, + Primitive: def.Primitive, + CullMode: def.CullMode, + }, + desc) + + instances := make([]Material, m.frames) + for i := range instances { + instance := mat.Instantiate(m.app.Pool()) + textures := cache.NewSamplerCache(m.app.Textures(), instance.Descriptors().Textures) + instances[i] = &DeferredMaterial{ + id: def.Hash(), + Instance: instance, + Objects: NewObjectBuffer(instance.Descriptors().Objects.Size), + Textures: textures, + Meshes: m.app.Meshes(), + } + } + + callback(instances) +} + +func (m *DeferredMatCache) Destroy() { + +} + +func (m *DeferredMatCache) Delete(mat []Material) { + mat[0].Destroy() +} diff --git a/engine/render/pass/deferred_geometry.go b/engine/render/pass/deferred_geometry.go new file mode 100644 index 0000000..cbd97c4 --- /dev/null +++ b/engine/render/pass/deferred_geometry.go @@ -0,0 +1,149 @@ +package pass + +import ( + "github.com/vkngwrapper/core/v2/core1_0" + "zworld/engine/object" + "zworld/engine/object/mesh" + "zworld/engine/renderapi" + "zworld/engine/renderapi/color" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/framebuffer" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/renderpass/attachment" + "zworld/engine/renderapi/vulkan" + "zworld/plugins/math/shape" +) + +const ( + DiffuseAttachment attachment.Name = "diffuse" + NormalsAttachment attachment.Name = "normals" + PositionAttachment attachment.Name = "position" + OutputAttachment attachment.Name = "output" +) + +type DeferredGeometryPass struct { + target vulkan.Target + gbuffer GeometryBuffer + app vulkan.App + pass renderpass.T + fbuf framebuffer.Array + + materials MaterialCache + meshQuery *object.Query[mesh.Mesh] +} + +func NewDeferredGeometryPass( + app vulkan.App, + depth vulkan.Target, + gbuffer GeometryBuffer, +) *DeferredGeometryPass { + pass := renderpass.New(app.Device(), renderpass.Args{ + Name: "Deferred Geometry", + ColorAttachments: []attachment.Color{ + { + Name: DiffuseAttachment, + LoadOp: core1_0.AttachmentLoadOpClear, + StoreOp: core1_0.AttachmentStoreOpStore, + FinalLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + Image: attachment.FromImageArray(gbuffer.Diffuse()), + }, + { + Name: NormalsAttachment, + LoadOp: core1_0.AttachmentLoadOpLoad, + StoreOp: core1_0.AttachmentStoreOpStore, + FinalLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + Image: attachment.FromImageArray(gbuffer.Normal()), + }, + { + Name: PositionAttachment, + LoadOp: core1_0.AttachmentLoadOpLoad, + StoreOp: core1_0.AttachmentStoreOpStore, + FinalLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + Image: attachment.FromImageArray(gbuffer.Position()), + }, + }, + DepthAttachment: &attachment.Depth{ + LoadOp: core1_0.AttachmentLoadOpLoad, + StencilLoadOp: core1_0.AttachmentLoadOpLoad, + StoreOp: core1_0.AttachmentStoreOpStore, + FinalLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + Image: attachment.FromImageArray(depth.Surfaces()), + }, + Subpasses: []renderpass.Subpass{ + { + Name: MainSubpass, + Depth: true, + + ColorAttachments: []attachment.Name{DiffuseAttachment, NormalsAttachment, PositionAttachment}, + }, + }, + }) + + fbuf, err := framebuffer.NewArray(gbuffer.Frames(), app.Device(), "deferred-geometry", gbuffer.Width(), gbuffer.Height(), pass) + if err != nil { + panic(err) + } + + app.Textures().Fetch(color.White) + + return &DeferredGeometryPass{ + gbuffer: gbuffer, + app: app, + pass: pass, + + fbuf: fbuf, + + materials: NewDeferredMaterialCache(app, pass, gbuffer.Frames()), + meshQuery: object.NewQuery[mesh.Mesh](), + } +} + +func (p *DeferredGeometryPass) Record(cmds command.Recorder, args renderapi.Args, scene object.Component) { + cmds.Record(func(cmd command.Buffer) { + cmd.CmdBeginRenderPass(p.pass, p.fbuf[args.Frame]) + }) + + frustum := shape.FrustumFromMatrix(args.VP) + + objects := p.meshQuery. + Reset(). + Where(isDrawDeferred). + Where(frustumCulled(&frustum)). + Collect(scene) + + cam := CameraFromArgs(args) + groups := MaterialGroups(p.materials, args.Frame, objects) + groups.Draw(cmds, cam, nil) + + cmds.Record(func(cmd command.Buffer) { + cmd.CmdEndRenderPass() + }) +} + +func (p *DeferredGeometryPass) Name() string { + return "Deferred" +} + +func (p *DeferredGeometryPass) Destroy() { + p.materials.Destroy() + p.materials = nil + p.fbuf.Destroy() + p.fbuf = nil + p.pass.Destroy() + p.pass = nil +} + +func isDrawDeferred(m mesh.Mesh) bool { + if mat := m.Material(); mat != nil { + return mat.Pass == material.Deferred + } + return false +} + +func frustumCulled(frustum *shape.Frustum) func(mesh.Mesh) bool { + return func(m mesh.Mesh) bool { + bounds := m.BoundingSphere() + return frustum.IntersectsSphere(&bounds) + } +} diff --git a/engine/render/pass/deferred_lighting.go b/engine/render/pass/deferred_lighting.go new file mode 100644 index 0000000..111282a --- /dev/null +++ b/engine/render/pass/deferred_lighting.go @@ -0,0 +1,146 @@ +package pass + +import ( + "github.com/vkngwrapper/core/v2/core1_0" + "zworld/engine/object" + "zworld/engine/object/light" + "zworld/engine/renderapi" + "zworld/engine/renderapi/cache" + "zworld/engine/renderapi/color" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/framebuffer" + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/renderpass/attachment" + "zworld/engine/renderapi/vertex" + "zworld/engine/renderapi/vulkan" +) + +const LightingSubpass renderpass.Name = "lighting" + +type DeferredLightPass struct { + app vulkan.App + target vulkan.Target + gbuffer GeometryBuffer + ssao vulkan.Target + quad vertex.Mesh + pass renderpass.T + light LightShader + fbuf framebuffer.Array + samplers []cache.SamplerCache + shadows []*ShadowCache + lightbufs []*LightBuffer + lightQuery *object.Query[light.T] +} + +func NewDeferredLightingPass( + app vulkan.App, + target vulkan.Target, + gbuffer GeometryBuffer, + shadows Shadow, + occlusion vulkan.Target, +) *DeferredLightPass { + pass := renderpass.New(app.Device(), renderpass.Args{ + Name: "Deferred Lighting", + ColorAttachments: []attachment.Color{ + { + Name: OutputAttachment, + Image: attachment.FromImageArray(target.Surfaces()), + Samples: 0, + LoadOp: core1_0.AttachmentLoadOpClear, + StoreOp: core1_0.AttachmentStoreOpStore, + InitialLayout: 0, + FinalLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + Clear: color.T{}, + Blend: attachment.BlendAdditive, + }, + }, + Subpasses: []renderpass.Subpass{ + { + Name: LightingSubpass, + + ColorAttachments: []attachment.Name{OutputAttachment}, + }, + }, + }) + + fbuf, err := framebuffer.NewArray(target.Frames(), app.Device(), "deferred-lighting", target.Width(), target.Height(), pass) + if err != nil { + panic(err) + } + + quad := vertex.ScreenQuad("geometry-pass-quad") + + lightsh := NewLightShader(app, pass, gbuffer, occlusion) + + samplers := make([]cache.SamplerCache, target.Frames()) + lightbufs := make([]*LightBuffer, target.Frames()) + shadowmaps := make([]*ShadowCache, target.Frames()) + for i := range lightbufs { + samplers[i] = cache.NewSamplerCache(app.Textures(), lightsh.Descriptors(i).Shadow) + shadowmaps[i] = NewShadowCache(samplers[i], shadows.Shadowmap) + lightbufs[i] = NewLightBuffer(256) + } + + return &DeferredLightPass{ + target: target, + gbuffer: gbuffer, + app: app, + quad: quad, + light: lightsh, + pass: pass, + fbuf: fbuf, + shadows: shadowmaps, + lightbufs: lightbufs, + lightQuery: object.NewQuery[light.T](), + } +} + +func (p *DeferredLightPass) Record(cmds command.Recorder, args renderapi.Args, scene object.Component) { + camera := CameraFromArgs(args) + + desc := p.light.Descriptors(args.Frame) + desc.Camera.Set(camera) + + lightbuf := p.lightbufs[args.Frame] + shadows := p.shadows[args.Frame] + lightbuf.Reset() + + // todo: perform frustum culling on light volumes + lights := p.lightQuery.Reset().Collect(scene) + for _, lit := range lights { + lightbuf.Store(lit.LightData(shadows)) + } + + lightbuf.Flush(desc.Lights) + shadows.Flush() + + quad := p.app.Meshes().Fetch(p.quad) + cmds.Record(func(cmd command.Buffer) { + cmd.CmdBeginRenderPass(p.pass, p.fbuf[args.Frame]) + + p.light.Bind(cmd, args.Frame) + + quad.Draw(cmd, 0) + + cmd.CmdEndRenderPass() + }) +} + +func (p *DeferredLightPass) Name() string { + return "Deferred Lighting" +} + +func (p *DeferredLightPass) Destroy() { + for _, cache := range p.samplers { + cache.Destroy() + } + p.samplers = nil + p.lightbufs = nil + + p.fbuf.Destroy() + p.fbuf = nil + p.pass.Destroy() + p.pass = nil + p.light.Destroy() + p.light = nil +} diff --git a/engine/render/pass/deferred_material.go b/engine/render/pass/deferred_material.go new file mode 100644 index 0000000..0753d26 --- /dev/null +++ b/engine/render/pass/deferred_material.go @@ -0,0 +1,70 @@ +package pass + +import ( + "zworld/engine/object/light" + "zworld/engine/object/mesh" + "zworld/engine/render/uniform" + "zworld/engine/renderapi/cache" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/material" +) + +type DeferredDescriptors struct { + descriptor.Set + Camera *descriptor.Uniform[uniform.Camera] + Objects *descriptor.Storage[uniform.Object] + Textures *descriptor.SamplerArray +} + +type DeferredMaterial struct { + Instance *material.Instance[*DeferredDescriptors] + Objects *ObjectBuffer + Textures cache.SamplerCache + Meshes cache.MeshCache + + id material.ID +} + +func (m *DeferredMaterial) ID() material.ID { + return m.id +} + +func (m *DeferredMaterial) Begin(camera uniform.Camera, lights []light.T) { + m.Instance.Descriptors().Camera.Set(camera) + m.Objects.Reset() +} + +func (m *DeferredMaterial) Bind(cmds command.Recorder) { + cmds.Record(func(cmd command.Buffer) { + m.Instance.Bind(cmd) + }) +} + +func (m *DeferredMaterial) End() { + m.Objects.Flush(m.Instance.Descriptors().Objects) + m.Textures.Flush() +} + +func (m *DeferredMaterial) Draw(cmds command.Recorder, msh mesh.Mesh) { + vkmesh, meshReady := m.Meshes.TryFetch(msh.Mesh().Get()) + if !meshReady { + return + } + + textures := m.Instance.Material().TextureSlots() + textureIds := AssignMeshTextures(m.Textures, msh, textures) + + index := m.Objects.Store(uniform.Object{ + Model: msh.Transform().Matrix(), + Textures: textureIds, + }) + + cmds.Record(func(cmd command.Buffer) { + vkmesh.Draw(cmd, index) + }) +} + +func (m *DeferredMaterial) Destroy() { + m.Instance.Material().Destroy() +} diff --git a/engine/render/pass/depth_cache.go b/engine/render/pass/depth_cache.go new file mode 100644 index 0000000..f5ea78e --- /dev/null +++ b/engine/render/pass/depth_cache.go @@ -0,0 +1,88 @@ +package pass + +import ( + "github.com/vkngwrapper/core/v2/core1_0" + "zworld/engine/render/uniform" + "zworld/engine/renderapi/cache" + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/shader" + "zworld/engine/renderapi/vertex" + "zworld/engine/renderapi/vulkan" +) + +type DepthMatCache struct { + app vulkan.App + pass renderpass.T + frames int +} + +func NewDepthMaterialCache(app vulkan.App, pass renderpass.T, frames int) MaterialCache { + return cache.New[*material.Def, []Material](&DepthMatCache{ + app: app, + pass: pass, + frames: frames, + }) +} + +func (m *DepthMatCache) Name() string { return "DepthMaterials" } + +func (m *DepthMatCache) Instantiate(def *material.Def, callback func([]Material)) { + if def == nil { + def = &material.Def{} + } + + desc := &BasicDescriptors{ + Camera: &descriptor.Uniform[uniform.Camera]{ + Stages: core1_0.StageAll, + }, + Objects: &descriptor.Storage[uniform.Object]{ + Stages: core1_0.StageAll, + Size: 2000, + }, + } + + // read vertex pointers from vertex format + pointers := vertex.ParsePointers(def.VertexFormat) + + // fetch shader from cache + shader := m.app.Shaders().Fetch(shader.NewRef("depth")) + + // create material + mat := material.New( + m.app.Device(), + material.Args{ + Shader: shader, + Pass: m.pass, + Subpass: MainSubpass, + Pointers: pointers, + CullMode: vertex.CullBack, + DepthTest: true, + DepthWrite: true, + DepthFunc: core1_0.CompareOpLess, + DepthClamp: def.DepthClamp, + Primitive: def.Primitive, + }, + desc) + + instances := make([]Material, m.frames) + for i := range instances { + instance := mat.Instantiate(m.app.Pool()) + instances[i] = &BasicMaterial{ + id: def.Hash(), + Instance: instance, + Objects: NewObjectBuffer(desc.Objects.Size), + Meshes: m.app.Meshes(), + } + } + + callback(instances) +} + +func (m *DepthMatCache) Destroy() { +} + +func (m *DepthMatCache) Delete(mat []Material) { + mat[0].Destroy() +} diff --git a/engine/render/pass/depth_pass.go b/engine/render/pass/depth_pass.go new file mode 100644 index 0000000..4ce1ac2 --- /dev/null +++ b/engine/render/pass/depth_pass.go @@ -0,0 +1,114 @@ +package pass + +import ( + "github.com/vkngwrapper/core/v2/core1_0" + "zworld/engine/object" + "zworld/engine/object/mesh" + "zworld/engine/renderapi" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/framebuffer" + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/renderpass/attachment" + "zworld/engine/renderapi/vulkan" +) + +type DepthPass struct { + gbuffer GeometryBuffer + app vulkan.App + pass renderpass.T + fbuf framebuffer.Array + + materials MaterialCache + meshQuery *object.Query[mesh.Mesh] +} + +var _ Pass = &ForwardPass{} + +func NewDepthPass( + app vulkan.App, + depth vulkan.Target, + gbuffer GeometryBuffer, +) *DepthPass { + pass := renderpass.New(app.Device(), renderpass.Args{ + Name: "Depth", + ColorAttachments: []attachment.Color{ + { + Name: NormalsAttachment, + LoadOp: core1_0.AttachmentLoadOpClear, + StoreOp: core1_0.AttachmentStoreOpStore, + FinalLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + + Image: attachment.FromImageArray(gbuffer.Normal()), + }, + { + Name: PositionAttachment, + LoadOp: core1_0.AttachmentLoadOpClear, + StoreOp: core1_0.AttachmentStoreOpStore, + FinalLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + + Image: attachment.FromImageArray(gbuffer.Position()), + }, + }, + DepthAttachment: &attachment.Depth{ + LoadOp: core1_0.AttachmentLoadOpClear, + StencilLoadOp: core1_0.AttachmentLoadOpClear, + StoreOp: core1_0.AttachmentStoreOpStore, + FinalLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + ClearDepth: 1, + + Image: attachment.FromImageArray(depth.Surfaces()), + }, + Subpasses: []renderpass.Subpass{ + { + Name: MainSubpass, + Depth: true, + + ColorAttachments: []attachment.Name{NormalsAttachment, PositionAttachment}, + }, + }, + }) + + fbuf, err := framebuffer.NewArray(gbuffer.Frames(), app.Device(), "depth", gbuffer.Width(), gbuffer.Height(), pass) + if err != nil { + panic(err) + } + + return &DepthPass{ + gbuffer: gbuffer, + app: app, + pass: pass, + fbuf: fbuf, + + materials: NewDepthMaterialCache(app, pass, gbuffer.Frames()), + meshQuery: object.NewQuery[mesh.Mesh](), + } +} + +func (p *DepthPass) Record(cmds command.Recorder, args renderapi.Args, scene object.Component) { + opaque := p.meshQuery. + Reset(). + Where(isDrawForward(false)). + Collect(scene) + + cmds.Record(func(cmd command.Buffer) { + cmd.CmdBeginRenderPass(p.pass, p.fbuf[args.Frame]) + }) + + cam := CameraFromArgs(args) + groups := MaterialGroups(p.materials, args.Frame, opaque) + groups.Draw(cmds, cam, nil) + + cmds.Record(func(cmd command.Buffer) { + cmd.CmdEndRenderPass() + }) +} + +func (p *DepthPass) Name() string { + return "Depth" +} + +func (p *DepthPass) Destroy() { + p.fbuf.Destroy() + p.pass.Destroy() + p.materials.Destroy() +} diff --git a/engine/render/pass/depth_sorter.go b/engine/render/pass/depth_sorter.go new file mode 100644 index 0000000..5ff6b11 --- /dev/null +++ b/engine/render/pass/depth_sorter.go @@ -0,0 +1,26 @@ +package pass + +import ( + "sort" + "zworld/engine/object/mesh" + "zworld/engine/render/uniform" + "zworld/plugins/math/vec3" +) + +func DepthSortGroups(cache MaterialCache, frame int, cam uniform.Camera, meshes []mesh.Mesh) DrawGroups { + eye := cam.Eye.XYZ() + + // perform back-to-front depth sorting + // we use the closest point on the meshes bounding sphere as a heuristic + sort.SliceStable(meshes, func(i, j int) bool { + // return true if meshes[i] is further away than meshes[j] + first, second := meshes[i].BoundingSphere(), meshes[j].BoundingSphere() + + di := vec3.Distance(eye, first.Center) - first.Radius + dj := vec3.Distance(eye, second.Center) - second.Radius + return di > dj + }) + + // sort meshes by material + return OrderedGroups(cache, frame, meshes) +} diff --git a/engine/render/pass/draw_group.go b/engine/render/pass/draw_group.go new file mode 100644 index 0000000..1faa623 --- /dev/null +++ b/engine/render/pass/draw_group.go @@ -0,0 +1,88 @@ +package pass + +import ( + "zworld/engine/object/light" + "zworld/engine/object/mesh" + "zworld/engine/render/uniform" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/material" +) + +type DrawGroup struct { + ID material.ID + Material Material + Meshes []mesh.Mesh +} + +type DrawGroups []DrawGroup + +func (groups DrawGroups) Draw(cmds command.Recorder, camera uniform.Camera, lights []light.T) { + for _, group := range groups { + // there could be multiple instances of the same material + // however, as long as there are no calls in between draws we should be okay + group.Material.Begin(camera, lights) + } + + for _, group := range groups { + group.Material.Bind(cmds) + for _, msh := range group.Meshes { + group.Material.Draw(cmds, msh) + } + } + + for _, group := range groups { + // can happen multiple times - similar to BeginFrame it should be ok + // it is wasted work though + group.Material.End() + } +} + +// Sort meshes by material according to depth. +// Consecutive meshes in the depth order are grouped if they have the same material +func OrderedGroups(cache MaterialCache, frame int, meshes []mesh.Mesh) DrawGroups { + groups := make(DrawGroups, 0, 16) + var group *DrawGroup + for _, msh := range meshes { + mats, ready := cache.TryFetch(msh.Material()) + if !ready { + continue + } + + id := msh.MaterialID() + if group == nil || id != group.Material.ID() { + groups = append(groups, DrawGroup{ + Material: mats[frame], + Meshes: make([]mesh.Mesh, 0, 32), + }) + group = &groups[len(groups)-1] + } + group.Meshes = append(group.Meshes, msh) + } + return groups +} + +// Sort meshes by material +func MaterialGroups(cache MaterialCache, frame int, meshes []mesh.Mesh) DrawGroups { + groups := make(DrawGroups, 0, 16) + matGroups := map[material.ID]*DrawGroup{} + + for _, msh := range meshes { + mats, ready := cache.TryFetch(msh.Material()) + if !ready { + continue + } + + group, exists := matGroups[msh.MaterialID()] + if !exists { + groups = append(groups, DrawGroup{ + Material: mats[frame], + Meshes: make([]mesh.Mesh, 0, 32), + }) + group = &groups[len(groups)-1] + matGroups[msh.MaterialID()] = group + } + group.Meshes = append(group.Meshes, msh) + } + + return groups +} diff --git a/engine/render/pass/effect/particle_pass.go b/engine/render/pass/effect/particle_pass.go new file mode 100644 index 0000000..4d27eb3 --- /dev/null +++ b/engine/render/pass/effect/particle_pass.go @@ -0,0 +1,133 @@ +package effect + +// import ( +// "github.com/go-gl/gl/v4.1-core/gl" +// "github.com/johanhenriksson/goworld/assets" +// "github.com/johanhenriksson/goworld/core/object" +// "github.com/johanhenriksson/goworld/core/transform" +// "github.com/johanhenriksson/goworld/math/random" +// "github.com/johanhenriksson/goworld/math/vec3" +// "github.com/johanhenriksson/goworld/render" +// "github.com/johanhenriksson/goworld/render/material" +// "github.com/johanhenriksson/goworld/render/vertex" +// ) + +// type ParticleDrawable interface { +// DrawParticles(render.Args) +// } + +// // ParticlePass represents the particle system draw pass +// type ParticlePass struct { +// } + +// // NewParticlePass creates a new particle system draw pass +// func NewParticlePass() *ParticlePass { +// return &ParticlePass{} +// } + +// // Resize is called on window resize. Should update any window size-dependent buffers +// func (p *ParticlePass) Resize(width, height int) {} + +// // DrawPass executes the particle pass +// func (p *ParticlePass) Draw(args render.Args, scene object.T) { + +// } + +// // Particle holds data about a single particle +// type Particle struct { +// Position vec3.T +// Velocity vec3.T +// Duration float32 +// } + +// // ParticleSystem holds the properties of a particle system effect +// type ParticleSystem struct { +// transform.T + +// Particles []Particle +// Count int +// Chance float32 +// MinVel vec3.T +// MaxVel vec3.T +// MinDur float32 +// MaxDur float32 + +// positions vec3.Array +// mat material.T +// vao vertex.Array +// } + +// // Update the particle system +// func (ps *ParticleSystem) Update(dt float32) { +// if len(ps.Particles) < ps.Count && random.Chance(ps.Chance) { +// // add particle + +// p := Particle{ +// Position: vec3.Zero, +// Velocity: vec3.Random(ps.MinVel, ps.MaxVel), +// Duration: random.Range(ps.MinDur, ps.MaxDur), +// } +// ps.Particles = append(ps.Particles, p) +// } + +// for i := 0; i < len(ps.Particles); i++ { +// if ps.Particles[i].Duration < 0 { +// // dead +// ps.remove(i) +// i-- +// } +// } + +// for i, p := range ps.Particles { +// ps.Particles[i].Duration -= dt +// ps.Particles[i].Position = p.Position.Add(p.Velocity.Scaled(dt)) +// ps.positions[i] = p.Position +// } + +// ps.vao.Buffer("geometry", ps.positions[:len(ps.Particles)]) +// } + +// func (ps *ParticleSystem) remove(i int) { +// ps.Particles[len(ps.Particles)-1], ps.Particles[i] = ps.Particles[i], ps.Particles[len(ps.Particles)-1] +// ps.Particles = ps.Particles[:len(ps.Particles)-1] +// } + +// // Draw the particle system +// func (ps *ParticleSystem) Draw(args render.Args) { +// args = args.Apply(ps.World()) + +// render.Blend(true) +// render.BlendFunc(gl.ONE, gl.ONE) +// render.DepthOutput(false) + +// ps.mat.Use() +// ps.mat.Vec3("eye", args.Position) +// ps.mat.Mat4("model", args.Transform) +// ps.mat.Mat4("vp", args.VP) +// ps.vao.Draw() + +// render.DepthOutput(true) +// } + +// // NewParticleSystem creates a new particle system +// func NewParticleSystem(position vec3.T) *ParticleSystem { +// count := 8 +// mat := assets.GetMaterial("billboard") +// ps := &ParticleSystem{ +// T: transform.New(position, vec3.Zero, vec3.One), + +// Count: count, +// Chance: 0.08, +// MinVel: vec3.New(-0.05, 0.4, -0.05), +// MaxVel: vec3.New(0.05, 0.6, 0.05), +// MinDur: 2, +// MaxDur: 3, + +// mat: mat, +// positions: make(vec3.Array, count), +// } + +// //mat.SetupVertexPointers() + +// return ps +// } diff --git a/engine/render/pass/forward.go b/engine/render/pass/forward.go new file mode 100644 index 0000000..b57b9e8 --- /dev/null +++ b/engine/render/pass/forward.go @@ -0,0 +1,131 @@ +package pass + +import ( + "github.com/vkngwrapper/core/v2/core1_0" + "zworld/engine/object" + "zworld/engine/object/light" + "zworld/engine/object/mesh" + "zworld/engine/renderapi" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/framebuffer" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/renderpass/attachment" + "zworld/engine/renderapi/vulkan" +) + +type ForwardPass struct { + target vulkan.Target + app vulkan.App + pass renderpass.T + fbuf framebuffer.Array + + materials MaterialCache + meshQuery *object.Query[mesh.Mesh] + lightQuery *object.Query[light.T] +} + +var _ Pass = &ForwardPass{} + +func NewForwardPass( + app vulkan.App, + target vulkan.Target, + depth vulkan.Target, + shadows Shadow, +) *ForwardPass { + pass := renderpass.New(app.Device(), renderpass.Args{ + Name: "Forward", + ColorAttachments: []attachment.Color{ + { + Name: OutputAttachment, + LoadOp: core1_0.AttachmentLoadOpLoad, + StoreOp: core1_0.AttachmentStoreOpStore, + FinalLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + Blend: attachment.BlendMultiply, + + Image: attachment.FromImageArray(target.Surfaces()), + }, + }, + DepthAttachment: &attachment.Depth{ + LoadOp: core1_0.AttachmentLoadOpLoad, + StencilLoadOp: core1_0.AttachmentLoadOpLoad, + StoreOp: core1_0.AttachmentStoreOpStore, + FinalLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + + Image: attachment.FromImageArray(depth.Surfaces()), + }, + Subpasses: []renderpass.Subpass{ + { + Name: MainSubpass, + Depth: true, + + ColorAttachments: []attachment.Name{OutputAttachment}, + }, + }, + }) + + fbuf, err := framebuffer.NewArray(target.Frames(), app.Device(), "forward", target.Width(), target.Height(), pass) + if err != nil { + panic(err) + } + + return &ForwardPass{ + target: target, + app: app, + pass: pass, + fbuf: fbuf, + + materials: NewForwardMaterialCache(app, pass, target.Frames(), shadows.Shadowmap), + meshQuery: object.NewQuery[mesh.Mesh](), + lightQuery: object.NewQuery[light.T](), + } +} + +func (p *ForwardPass) Record(cmds command.Recorder, args renderapi.Args, scene object.Component) { + cam := CameraFromArgs(args) + lights := p.lightQuery.Reset().Collect(scene) + + cmds.Record(func(cmd command.Buffer) { + cmd.CmdBeginRenderPass(p.pass, p.fbuf[args.Frame]) + }) + + // opaque pass + opaque := p.meshQuery. + Reset(). + Where(isDrawForward(false)). + Collect(scene) + groups := MaterialGroups(p.materials, args.Frame, opaque) + groups.Draw(cmds, cam, lights) + + // transparent pass + transparent := p.meshQuery. + Reset(). + Where(isDrawForward(true)). + Where(func(m mesh.Mesh) bool { return m.Material().Transparent }). + Collect(scene) + groups = DepthSortGroups(p.materials, args.Frame, cam, transparent) + groups.Draw(cmds, cam, lights) + + cmds.Record(func(cmd command.Buffer) { + cmd.CmdEndRenderPass() + }) +} + +func (p *ForwardPass) Name() string { + return "Forward" +} + +func (p *ForwardPass) Destroy() { + p.fbuf.Destroy() + p.pass.Destroy() + p.materials.Destroy() +} + +func isDrawForward(transparent bool) func(m mesh.Mesh) bool { + return func(m mesh.Mesh) bool { + if mat := m.Material(); mat != nil { + return mat.Pass == material.Forward && m.Material().Transparent == transparent + } + return false + } +} diff --git a/engine/render/pass/forward_cache.go b/engine/render/pass/forward_cache.go new file mode 100644 index 0000000..4990044 --- /dev/null +++ b/engine/render/pass/forward_cache.go @@ -0,0 +1,104 @@ +package pass + +import ( + "github.com/vkngwrapper/core/v2/core1_0" + "zworld/engine/render/uniform" + "zworld/engine/renderapi/cache" + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/shader" + "zworld/engine/renderapi/vertex" + "zworld/engine/renderapi/vulkan" +) + +type ForwardMatCache struct { + app vulkan.App + pass renderpass.T + lookup ShadowmapLookupFn + frames int +} + +func NewForwardMaterialCache(app vulkan.App, pass renderpass.T, frames int, lookup ShadowmapLookupFn) MaterialCache { + return cache.New[*material.Def, []Material](&ForwardMatCache{ + app: app, + pass: pass, + lookup: lookup, + frames: frames, + }) +} + +func (m *ForwardMatCache) Name() string { return "ForwardMaterials" } + +func (m *ForwardMatCache) Instantiate(def *material.Def, callback func([]Material)) { + if def == nil { + def = material.StandardForward() + } + + desc := &ForwardDescriptors{ + Camera: &descriptor.Uniform[uniform.Camera]{ + Stages: core1_0.StageAll, + }, + Objects: &descriptor.Storage[uniform.Object]{ + Stages: core1_0.StageAll, + Size: 2000, + }, + Lights: &descriptor.Storage[uniform.Light]{ + Stages: core1_0.StageAll, + Size: 256, + }, + Textures: &descriptor.SamplerArray{ + Stages: core1_0.StageFragment, + Count: 100, + }, + } + + // read vertex pointers from vertex format + pointers := vertex.ParsePointers(def.VertexFormat) + + // fetch shader from cache + shader := m.app.Shaders().Fetch(shader.NewRef(def.Shader)) + + // create material + mat := material.New( + m.app.Device(), + material.Args{ + Shader: shader, + Pass: m.pass, + Subpass: MainSubpass, + Pointers: pointers, + DepthTest: def.DepthTest, + DepthWrite: def.DepthWrite, + DepthClamp: def.DepthClamp, + DepthFunc: def.DepthFunc, + Primitive: def.Primitive, + CullMode: def.CullMode, + }, + desc) + + instances := make([]Material, m.frames) + for i := range instances { + instance := mat.Instantiate(m.app.Pool()) + textures := cache.NewSamplerCache(m.app.Textures(), instance.Descriptors().Textures) + + instances[i] = &ForwardMaterial{ + id: def.Hash(), + Instance: instance, + Objects: NewObjectBuffer(desc.Objects.Size), + Lights: NewLightBuffer(desc.Lights.Size), + Shadows: NewShadowCache(textures, m.lookup), + Textures: textures, + Meshes: m.app.Meshes(), + } + } + + callback(instances) +} + +func (m *ForwardMatCache) Destroy() { + +} + +func (m *ForwardMatCache) Delete(mat []Material) { + mat[0].Destroy() +} diff --git a/engine/render/pass/forward_material.go b/engine/render/pass/forward_material.go new file mode 100644 index 0000000..d5b5958 --- /dev/null +++ b/engine/render/pass/forward_material.go @@ -0,0 +1,85 @@ +package pass + +import ( + "zworld/engine/object/light" + "zworld/engine/object/mesh" + "zworld/engine/render/uniform" + "zworld/engine/renderapi/cache" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/material" +) + +type ForwardDescriptors struct { + descriptor.Set + Camera *descriptor.Uniform[uniform.Camera] + Objects *descriptor.Storage[uniform.Object] + Lights *descriptor.Storage[uniform.Light] + Textures *descriptor.SamplerArray +} + +type ForwardMaterial struct { + Instance *material.Instance[*ForwardDescriptors] + Objects *ObjectBuffer + Lights *LightBuffer + Shadows *ShadowCache + Textures cache.SamplerCache + Meshes cache.MeshCache + + id material.ID +} + +func (m *ForwardMaterial) ID() material.ID { + return m.id +} + +func (m *ForwardMaterial) Begin(camera uniform.Camera, lights []light.T) { + m.Instance.Descriptors().Camera.Set(camera) + + // multiple calls to this reset in a single frame will cause weird behaviour + // we need to split this function somehow in order to be able to do depth sorting etc + m.Objects.Reset() + + if len(lights) > 0 { + // how to get ambient light info? + m.Lights.Reset() + for _, lit := range lights { + m.Lights.Store(lit.LightData(m.Shadows)) + } + m.Lights.Flush(m.Instance.Descriptors().Lights) + } +} + +func (m *ForwardMaterial) Bind(cmds command.Recorder) { + cmds.Record(func(cmd command.Buffer) { + m.Instance.Bind(cmd) + }) +} + +func (m *ForwardMaterial) End() { + m.Objects.Flush(m.Instance.Descriptors().Objects) + m.Textures.Flush() +} + +func (m *ForwardMaterial) Draw(cmds command.Recorder, msh mesh.Mesh) { + vkmesh, meshReady := m.Meshes.TryFetch(msh.Mesh().Get()) + if !meshReady { + return + } + + textures := m.Instance.Material().TextureSlots() + textureIds := AssignMeshTextures(m.Textures, msh, textures) + + index := m.Objects.Store(uniform.Object{ + Model: msh.Transform().Matrix(), + Textures: textureIds, + }) + + cmds.Record(func(cmd command.Buffer) { + vkmesh.Draw(cmd, index) + }) +} + +func (m *ForwardMaterial) Destroy() { + m.Instance.Material().Destroy() +} diff --git a/engine/render/pass/gbuffer.go b/engine/render/pass/gbuffer.go new file mode 100644 index 0000000..5b197a3 --- /dev/null +++ b/engine/render/pass/gbuffer.go @@ -0,0 +1,94 @@ +package pass + +import ( + "github.com/vkngwrapper/core/v2/core1_0" + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/image" + "zworld/engine/renderapi/vulkan" + "zworld/plugins/math/vec2" +) + +type GeometryBuffer interface { + Width() int + Height() int + Frames() int + Diffuse() []image.T + Normal() []image.T + Position() []image.T + Destroy() +} + +type gbuffer struct { + diffuse []image.T + normal []image.T + position []image.T + width int + height int +} + +func NewGbuffer(device device.T, size vulkan.TargetSize) (GeometryBuffer, error) { + frames, width, height := size.Frames, size.Width, size.Height + diffuseFmt := core1_0.FormatR8G8B8A8UnsignedNormalized + normalFmt := core1_0.FormatR8G8B8A8UnsignedNormalized + positionFmt := core1_0.FormatR32G32B32A32SignedFloat + usage := core1_0.ImageUsageSampled | core1_0.ImageUsageColorAttachment | core1_0.ImageUsageInputAttachment + + var err error + diffuses := make([]image.T, frames) + normals := make([]image.T, frames) + positions := make([]image.T, frames) + + for i := 0; i < frames; i++ { + diffuses[i], err = image.New2D(device, "diffuse", width, height, diffuseFmt, usage) + if err != nil { + return nil, err + } + + normals[i], err = image.New2D(device, "normal", width, height, normalFmt, usage|core1_0.ImageUsageTransferSrc) + if err != nil { + return nil, err + } + + positions[i], err = image.New2D(device, "position", width, height, positionFmt, usage|core1_0.ImageUsageTransferSrc) + if err != nil { + return nil, err + } + } + + return &gbuffer{ + diffuse: diffuses, + normal: normals, + position: positions, + width: width, + height: height, + }, nil +} + +func (b *gbuffer) Width() int { return b.width } +func (b *gbuffer) Height() int { return b.height } +func (b *gbuffer) Frames() int { return len(b.diffuse) } +func (b *gbuffer) Diffuse() []image.T { return b.diffuse } +func (b *gbuffer) Normal() []image.T { return b.normal } +func (b *gbuffer) Position() []image.T { return b.position } + +func (b *gbuffer) pixelOffset(pos vec2.T, img image.T, size int) int { + denormPos := pos.Mul(img.Size().XY()) + return size * (int(denormPos.Y)*img.Width() + int(denormPos.X)) +} + +func (p *gbuffer) Destroy() { + for _, img := range p.diffuse { + img.Destroy() + } + p.diffuse = nil + + for _, img := range p.normal { + img.Destroy() + } + p.normal = nil + + for _, img := range p.position { + img.Destroy() + } + p.position = nil +} diff --git a/engine/render/pass/light_buffer.go b/engine/render/pass/light_buffer.go new file mode 100644 index 0000000..97187f5 --- /dev/null +++ b/engine/render/pass/light_buffer.go @@ -0,0 +1,50 @@ +package pass + +import ( + "unsafe" + "zworld/engine/object/light" + "zworld/engine/render/uniform" + "zworld/engine/renderapi/color" + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/texture" +) + +type ShadowmapLookupFn func(light.T, int) texture.T + +type LightBuffer struct { + buffer []uniform.Light + settings uniform.LightSettings +} + +func NewLightBuffer(capacity int) *LightBuffer { + return &LightBuffer{ + buffer: make([]uniform.Light, 1, capacity+1), + + // default lighting settings + settings: uniform.LightSettings{ + AmbientColor: color.White, + AmbientIntensity: 0.4, + + ShadowBias: 0.005, + ShadowSampleRadius: 1, + ShadowSamples: 1, + NormalOffset: 0.1, + }, + } +} + +func (b *LightBuffer) Flush(desc *descriptor.Storage[uniform.Light]) { + // settings is stored in the first element of the buffer + // it excludes the first element containing the light settings + b.settings.Count = int32(len(b.buffer) - 1) + b.buffer[0] = *(*uniform.Light)(unsafe.Pointer(&b.settings)) + desc.SetRange(0, b.buffer) +} + +func (b *LightBuffer) Reset() { + b.buffer = b.buffer[:1] +} + +func (b *LightBuffer) Store(light uniform.Light) { + b.buffer = append(b.buffer, light) +} diff --git a/engine/render/pass/light_shader.go b/engine/render/pass/light_shader.go new file mode 100644 index 0000000..94e074f --- /dev/null +++ b/engine/render/pass/light_shader.go @@ -0,0 +1,152 @@ +package pass + +import ( + "github.com/vkngwrapper/core/v2/core1_0" + "zworld/engine/render/uniform" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/shader" + "zworld/engine/renderapi/texture" + "zworld/engine/renderapi/vertex" + "zworld/engine/renderapi/vulkan" +) + +type LightDescriptors struct { + descriptor.Set + Camera *descriptor.Uniform[uniform.Camera] + Lights *descriptor.Storage[uniform.Light] + Diffuse *descriptor.Sampler + Normal *descriptor.Sampler + Position *descriptor.Sampler + Occlusion *descriptor.Sampler + Shadow *descriptor.SamplerArray +} + +type LightShader interface { + Bind(command.Buffer, int) + Descriptors(int) *LightDescriptors + Destroy() +} + +type lightShader struct { + mat *material.Material[*LightDescriptors] + instances []*material.Instance[*LightDescriptors] + + diffuseTex []texture.T + normalTex []texture.T + positionTex []texture.T + occlusionTex []texture.T +} + +func NewLightShader(app vulkan.App, pass renderpass.T, gbuffer GeometryBuffer, occlusion vulkan.Target) LightShader { + mat := material.New( + app.Device(), + material.Args{ + Shader: app.Shaders().Fetch(shader.NewRef("light")), + Pass: pass, + Subpass: LightingSubpass, + Pointers: vertex.ParsePointers(vertex.T{}), + DepthTest: false, + }, + &LightDescriptors{ + Camera: &descriptor.Uniform[uniform.Camera]{ + Stages: core1_0.StageFragment, + }, + Lights: &descriptor.Storage[uniform.Light]{ + Stages: core1_0.StageFragment, + Size: 256, + }, + Diffuse: &descriptor.Sampler{ + Stages: core1_0.StageFragment, + }, + Normal: &descriptor.Sampler{ + Stages: core1_0.StageFragment, + }, + Position: &descriptor.Sampler{ + Stages: core1_0.StageFragment, + }, + Occlusion: &descriptor.Sampler{ + Stages: core1_0.StageFragment, + }, + Shadow: &descriptor.SamplerArray{ + Stages: core1_0.StageFragment, + Count: 32, + }, + }) + + frames := gbuffer.Frames() + lightsh := mat.InstantiateMany(app.Pool(), frames) + + var err error + diffuseTex := make([]texture.T, frames) + normalTex := make([]texture.T, frames) + positionTex := make([]texture.T, frames) + occlusionTex := make([]texture.T, frames) + for i := 0; i < frames; i++ { + diffuseTex[i], err = texture.FromImage(app.Device(), "deferred-diffuse", gbuffer.Diffuse()[i], texture.Args{ + Filter: texture.FilterNearest, + }) + if err != nil { + panic(err) + } + normalTex[i], err = texture.FromImage(app.Device(), "deferred-normal", gbuffer.Normal()[i], texture.Args{ + Filter: texture.FilterNearest, + }) + if err != nil { + panic(err) + } + positionTex[i], err = texture.FromImage(app.Device(), "deferred-position", gbuffer.Position()[i], texture.Args{ + Filter: texture.FilterNearest, + }) + if err != nil { + panic(err) + } + occlusionTex[i], err = texture.FromImage(app.Device(), "deferred-ssao", occlusion.Surfaces()[i], texture.Args{ + Filter: texture.FilterNearest, + }) + if err != nil { + panic(err) + } + + lightDesc := lightsh[i].Descriptors() + lightDesc.Diffuse.Set(diffuseTex[i]) + lightDesc.Normal.Set(normalTex[i]) + lightDesc.Position.Set(positionTex[i]) + lightDesc.Occlusion.Set(occlusionTex[i]) + } + + return &lightShader{ + mat: mat, + instances: lightsh, + + diffuseTex: diffuseTex, + normalTex: normalTex, + positionTex: positionTex, + occlusionTex: occlusionTex, + } +} + +func (ls *lightShader) Bind(buf command.Buffer, frame int) { + ls.instances[frame].Bind(buf) +} +func (ls *lightShader) Descriptors(frame int) *LightDescriptors { + return ls.instances[frame].Descriptors() +} + +func (ls *lightShader) Destroy() { + for _, view := range ls.diffuseTex { + view.Destroy() + } + for _, view := range ls.normalTex { + view.Destroy() + } + for _, view := range ls.positionTex { + view.Destroy() + } + for _, view := range ls.occlusionTex { + view.Destroy() + } + ls.mat.Destroy() +} diff --git a/engine/render/pass/line_cache.go b/engine/render/pass/line_cache.go new file mode 100644 index 0000000..7810f30 --- /dev/null +++ b/engine/render/pass/line_cache.go @@ -0,0 +1,88 @@ +package pass + +import ( + "github.com/vkngwrapper/core/v2/core1_0" + "zworld/engine/render/uniform" + "zworld/engine/renderapi/cache" + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/shader" + "zworld/engine/renderapi/vertex" + "zworld/engine/renderapi/vulkan" +) + +type LineMatCache struct { + app vulkan.App + pass renderpass.T + frames int +} + +func NewLineMaterialCache(app vulkan.App, pass renderpass.T, frames int) MaterialCache { + return cache.New[*material.Def, []Material](&LineMatCache{ + app: app, + pass: pass, + frames: frames, + }) +} + +func (m *LineMatCache) Name() string { return "LineMaterials" } + +func (m *LineMatCache) Instantiate(def *material.Def, callback func([]Material)) { + if def == nil { + def = material.Lines() + } + + desc := &BasicDescriptors{ + Camera: &descriptor.Uniform[uniform.Camera]{ + Stages: core1_0.StageAll, + }, + Objects: &descriptor.Storage[uniform.Object]{ + Stages: core1_0.StageAll, + Size: 2000, + }, + } + + // read vertex pointers from vertex format + pointers := vertex.ParsePointers(def.VertexFormat) + + // fetch shader from cache + shader := m.app.Shaders().Fetch(shader.NewRef(def.Shader)) + + // create material + mat := material.New( + m.app.Device(), + material.Args{ + Shader: shader, + Pass: m.pass, + Subpass: MainSubpass, + Pointers: pointers, + DepthTest: def.DepthTest, + DepthWrite: def.DepthWrite, + DepthClamp: def.DepthClamp, + DepthFunc: def.DepthFunc, + Primitive: def.Primitive, + CullMode: def.CullMode, + }, + desc) + + instances := make([]Material, m.frames) + for i := range instances { + instance := mat.Instantiate(m.app.Pool()) + instances[i] = &BasicMaterial{ + id: def.Hash(), + Instance: instance, + Objects: NewObjectBuffer(desc.Objects.Size), + Meshes: m.app.Meshes(), + } + } + + callback(instances) +} + +func (m *LineMatCache) Destroy() { +} + +func (m *LineMatCache) Delete(mat []Material) { + mat[0].Destroy() +} diff --git a/engine/render/pass/lines.go b/engine/render/pass/lines.go new file mode 100644 index 0000000..2e03b6c --- /dev/null +++ b/engine/render/pass/lines.go @@ -0,0 +1,111 @@ +package pass + +import ( + "github.com/vkngwrapper/core/v2/core1_0" + "log" + "zworld/engine/object" + "zworld/engine/object/mesh" + "zworld/engine/renderapi" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/framebuffer" + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/renderpass/attachment" + "zworld/engine/renderapi/vertex" + "zworld/engine/renderapi/vulkan" + lineShape "zworld/plugins/geometry/lines" +) + +type LinePass struct { + app vulkan.App + target vulkan.Target + pass renderpass.T + fbuf framebuffer.Array + materials MaterialCache + meshQuery *object.Query[mesh.Mesh] +} + +func NewLinePass(app vulkan.App, target vulkan.Target, depth vulkan.Target) *LinePass { + log.Println("create line pass") + + pass := renderpass.New(app.Device(), renderpass.Args{ + Name: "Lines", + ColorAttachments: []attachment.Color{ + { + Name: OutputAttachment, + Image: attachment.FromImageArray(target.Surfaces()), + LoadOp: core1_0.AttachmentLoadOpLoad, + StoreOp: core1_0.AttachmentStoreOpStore, + InitialLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + FinalLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + Blend: attachment.BlendMix, + }, + }, + DepthAttachment: &attachment.Depth{ + Image: attachment.FromImageArray(depth.Surfaces()), + LoadOp: core1_0.AttachmentLoadOpLoad, + InitialLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + FinalLayout: core1_0.ImageLayoutDepthStencilAttachmentOptimal, + }, + Subpasses: []renderpass.Subpass{ + { + Name: MainSubpass, + Depth: true, + + ColorAttachments: []attachment.Name{OutputAttachment}, + }, + }, + }) + + fbufs, err := framebuffer.NewArray(target.Frames(), app.Device(), "lines", target.Width(), target.Height(), pass) + if err != nil { + panic(err) + } + + lineShape.Debug.Setup(target.Frames()) + + return &LinePass{ + app: app, + target: target, + pass: pass, + fbuf: fbufs, + materials: NewLineMaterialCache(app, pass, target.Frames()), + meshQuery: object.NewQuery[mesh.Mesh](), + } +} + +func (p *LinePass) Record(cmds command.Recorder, args renderapi.Args, scene object.Component) { + cmds.Record(func(cmd command.Buffer) { + cmd.CmdBeginRenderPass(p.pass, p.fbuf[args.Frame]) + }) + + lines := p.meshQuery. + Reset(). + Where(isDrawLines). + Collect(scene) + + // debug lines + debug := lineShape.Debug.Fetch() + lines = append(lines, debug) + + cam := CameraFromArgs(args) + groups := MaterialGroups(p.materials, args.Frame, lines) + groups.Draw(cmds, cam, nil) + + cmds.Record(func(cmd command.Buffer) { + cmd.CmdEndRenderPass() + }) +} + +func (p *LinePass) Name() string { + return "Lines" +} + +func (p *LinePass) Destroy() { + p.fbuf.Destroy() + p.pass.Destroy() + p.materials.Destroy() +} + +func isDrawLines(m mesh.Mesh) bool { + return m.Primitive() == vertex.Lines +} diff --git a/engine/render/pass/material.go b/engine/render/pass/material.go new file mode 100644 index 0000000..b1f650c --- /dev/null +++ b/engine/render/pass/material.go @@ -0,0 +1,68 @@ +package pass + +import ( + "zworld/engine/object/light" + "zworld/engine/object/mesh" + "zworld/engine/render/uniform" + "zworld/engine/renderapi" + "zworld/engine/renderapi/cache" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/texture" + "zworld/plugins/math/vec2" + "zworld/plugins/math/vec4" +) + +type MaterialCache cache.T[*material.Def, []Material] + +// Material implements render logic for a specific material. +type Material interface { + ID() material.ID + Destroy() + + // Begin is called prior to drawing, once per frame. + // Its purpose is to clear object buffers and set up per-frame data such as cameras & lighting. + Begin(uniform.Camera, []light.T) + + // BeginGroup is called just prior to recording draw calls. + // Its called once for each group, and may be called multiple times each frame. + // The primary use for it is to bind the material prior to drawing each group. + Bind(command.Recorder) + + // Draw is called for each mesh in the group. + // Its purpose is to set up per-draw data such as object transforms and textures + // as well as issuing the draw call. + Draw(command.Recorder, mesh.Mesh) + + // End is called after all draw groups have been processed. + // It runs once per frame and is primarily responsible for flushing uniform buffers. + End() +} + +func AssignMeshTextures(samplers cache.SamplerCache, msh mesh.Mesh, slots []texture.Slot) [4]uint32 { + textureIds := [4]uint32{} + for id, slot := range slots { + ref := msh.Texture(slot) + if ref != nil { + handle, exists := samplers.TryFetch(ref) + if exists { + textureIds[id] = uint32(handle.ID) + } + } + } + return textureIds +} + +func CameraFromArgs(args renderapi.Args) uniform.Camera { + return uniform.Camera{ + Proj: args.Projection, + View: args.View, + ViewProj: args.VP, + ProjInv: args.Projection.Invert(), + ViewInv: args.View.Invert(), + ViewProjInv: args.VP.Invert(), + Eye: vec4.Extend(args.Position, 0), + Forward: vec4.Extend(args.Forward, 0), + Viewport: vec2.NewI(args.Viewport.Width, args.Viewport.Height), + } +} diff --git a/engine/render/pass/object_buffer.go b/engine/render/pass/object_buffer.go new file mode 100644 index 0000000..3585562 --- /dev/null +++ b/engine/render/pass/object_buffer.go @@ -0,0 +1,30 @@ +package pass + +import ( + "zworld/engine/render/uniform" + "zworld/engine/renderapi/descriptor" +) + +type ObjectBuffer struct { + buffer []uniform.Object +} + +func NewObjectBuffer(capacity int) *ObjectBuffer { + return &ObjectBuffer{ + buffer: make([]uniform.Object, 0, capacity), + } +} + +func (b *ObjectBuffer) Flush(desc *descriptor.Storage[uniform.Object]) { + desc.SetRange(0, b.buffer) +} + +func (b *ObjectBuffer) Reset() { + b.buffer = b.buffer[:0] +} + +func (b *ObjectBuffer) Store(light uniform.Object) int { + index := len(b.buffer) + b.buffer = append(b.buffer, light) + return index +} diff --git a/engine/render/pass/output.go b/engine/render/pass/output.go new file mode 100644 index 0000000..42a9438 --- /dev/null +++ b/engine/render/pass/output.go @@ -0,0 +1,130 @@ +package pass + +import ( + "fmt" + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/extensions/v2/khr_swapchain" + "log" + "zworld/engine/object" + "zworld/engine/renderapi" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/framebuffer" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/renderpass/attachment" + "zworld/engine/renderapi/shader" + "zworld/engine/renderapi/texture" + "zworld/engine/renderapi/vertex" + "zworld/engine/renderapi/vulkan" +) + +type OutputPass struct { + app vulkan.App + material *material.Material[*OutputDescriptors] + source vulkan.Target + + quad vertex.Mesh + desc []*material.Instance[*OutputDescriptors] + tex []texture.T + fbufs framebuffer.Array + pass renderpass.T +} + +var _ Pass = &OutputPass{} + +type OutputDescriptors struct { + descriptor.Set + Output *descriptor.Sampler +} + +func NewOutputPass(app vulkan.App, target vulkan.Target, source vulkan.Target) *OutputPass { + log.Println("create output pass") + p := &OutputPass{ + app: app, + source: source, + } + + p.quad = vertex.ScreenQuad("output-pass-quad") + + p.pass = renderpass.New(app.Device(), renderpass.Args{ + Name: "Output", + ColorAttachments: []attachment.Color{ + { + Name: OutputAttachment, + Image: attachment.FromImageArray(target.Surfaces()), + LoadOp: core1_0.AttachmentLoadOpClear, // clearing avoids displaying garbage on the very first frame + FinalLayout: khr_swapchain.ImageLayoutPresentSrc, + }, + }, + Subpasses: []renderpass.Subpass{ + { + Name: MainSubpass, + ColorAttachments: []attachment.Name{OutputAttachment}, + }, + }, + }) + + p.material = material.New( + app.Device(), + material.Args{ + Shader: app.Shaders().Fetch(shader.NewRef("output")), + Pass: p.pass, + Pointers: vertex.ParsePointers(vertex.T{}), + DepthTest: false, + DepthWrite: false, + }, + &OutputDescriptors{ + Output: &descriptor.Sampler{ + Stages: core1_0.StageFragment, + }, + }) + + frames := target.Frames() + var err error + p.fbufs, err = framebuffer.NewArray(frames, app.Device(), "output", target.Width(), target.Height(), p.pass) + if err != nil { + panic(err) + } + + p.desc = p.material.InstantiateMany(app.Pool(), frames) + p.tex = make([]texture.T, frames) + for i := range p.tex { + key := fmt.Sprintf("gbuffer-output-%d", i) + p.tex[i], err = texture.FromImage(app.Device(), key, p.source.Surfaces()[i], texture.Args{ + Filter: texture.FilterNearest, + Wrap: texture.WrapClamp, + }) + if err != nil { + // todo: clean up + panic(err) + } + p.desc[i].Descriptors().Output.Set(p.tex[i]) + } + + return p +} + +func (p *OutputPass) Record(cmds command.Recorder, args renderapi.Args, scene object.Component) { + quad := p.app.Meshes().Fetch(p.quad) + + cmds.Record(func(cmd command.Buffer) { + cmd.CmdBeginRenderPass(p.pass, p.fbufs[args.Frame]) + p.desc[args.Frame].Bind(cmd) + quad.Draw(cmd, 0) + cmd.CmdEndRenderPass() + }) +} + +func (p *OutputPass) Name() string { + return "Output" +} + +func (p *OutputPass) Destroy() { + for _, tex := range p.tex { + tex.Destroy() + } + p.fbufs.Destroy() + p.pass.Destroy() + p.material.Destroy() +} diff --git a/engine/render/pass/pass.go b/engine/render/pass/pass.go new file mode 100644 index 0000000..623d032 --- /dev/null +++ b/engine/render/pass/pass.go @@ -0,0 +1,16 @@ +package pass + +import ( + "zworld/engine/object" + "zworld/engine/renderapi" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/renderpass" +) + +const MainSubpass = renderpass.Name("main") + +type Pass interface { + Name() string + Record(command.Recorder, renderapi.Args, object.Component) + Destroy() +} diff --git a/engine/render/pass/postprocess.go b/engine/render/pass/postprocess.go new file mode 100644 index 0000000..9086904 --- /dev/null +++ b/engine/render/pass/postprocess.go @@ -0,0 +1,143 @@ +package pass + +import ( + "fmt" + "github.com/vkngwrapper/core/v2/core1_0" + "zworld/engine/object" + "zworld/engine/renderapi" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/framebuffer" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/renderpass/attachment" + "zworld/engine/renderapi/shader" + "zworld/engine/renderapi/texture" + "zworld/engine/renderapi/vertex" + "zworld/engine/renderapi/vulkan" +) + +type PostProcessPass struct { + LUT texture.Ref + + app vulkan.App + input vulkan.Target + + quad vertex.Mesh + mat *material.Material[*PostProcessDescriptors] + desc []*material.Instance[*PostProcessDescriptors] + fbufs framebuffer.Array + pass renderpass.T + + inputTex []texture.T +} + +var _ Pass = &PostProcessPass{} + +type PostProcessDescriptors struct { + descriptor.Set + Input *descriptor.Sampler + LUT *descriptor.Sampler +} + +func NewPostProcessPass(app vulkan.App, target vulkan.Target, input vulkan.Target) *PostProcessPass { + var err error + p := &PostProcessPass{ + LUT: texture.PathRef("textures/color_grading/none.png"), + + app: app, + input: input, + } + + p.quad = vertex.ScreenQuad("blur-pass-quad") + + p.pass = renderpass.New(app.Device(), renderpass.Args{ + Name: "PostProcess", + ColorAttachments: []attachment.Color{ + { + Name: OutputAttachment, + Image: attachment.FromImageArray(target.Surfaces()), + LoadOp: core1_0.AttachmentLoadOpDontCare, + FinalLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + }, + }, + Subpasses: []renderpass.Subpass{ + { + Name: MainSubpass, + ColorAttachments: []attachment.Name{OutputAttachment}, + }, + }, + }) + + p.mat = material.New( + app.Device(), + material.Args{ + Shader: app.Shaders().Fetch(shader.NewRef("postprocess")), + Pass: p.pass, + Pointers: vertex.ParsePointers(vertex.T{}), + DepthTest: false, + DepthWrite: false, + }, + &PostProcessDescriptors{ + Input: &descriptor.Sampler{ + Stages: core1_0.StageFragment, + }, + LUT: &descriptor.Sampler{ + Stages: core1_0.StageFragment, + }, + }) + + frames := input.Frames() + p.fbufs, err = framebuffer.NewArray(frames, app.Device(), "blur", target.Width(), target.Height(), p.pass) + if err != nil { + panic(err) + } + + p.desc = p.mat.InstantiateMany(app.Pool(), frames) + p.inputTex = make([]texture.T, frames) + for i := 0; i < input.Frames(); i++ { + inputKey := fmt.Sprintf("post-input-%d", i) + p.inputTex[i], err = texture.FromImage(app.Device(), inputKey, p.input.Surfaces()[i], texture.Args{ + Filter: texture.FilterNearest, + Wrap: texture.WrapClamp, + }) + if err != nil { + // todo: clean up + panic(err) + } + p.desc[i].Descriptors().Input.Set(p.inputTex[i]) + } + + return p +} + +func (p *PostProcessPass) Record(cmds command.Recorder, args renderapi.Args, scene object.Component) { + quad := p.app.Meshes().Fetch(p.quad) + + // refresh color lut + lutTex := p.app.Textures().Fetch(p.LUT) + + cmds.Record(func(cmd command.Buffer) { + cmd.CmdBeginRenderPass(p.pass, p.fbufs[args.Frame]) + + desc := p.desc[args.Frame] + desc.Bind(cmd) + desc.Descriptors().LUT.Set(lutTex) + + quad.Draw(cmd, 0) + cmd.CmdEndRenderPass() + }) +} + +func (p *PostProcessPass) Name() string { + return "PostProcess" +} + +func (p *PostProcessPass) Destroy() { + for _, tex := range p.inputTex { + tex.Destroy() + } + p.fbufs.Destroy() + p.pass.Destroy() + p.mat.Destroy() +} diff --git a/engine/render/pass/shadow.go b/engine/render/pass/shadow.go new file mode 100644 index 0000000..38a3aaf --- /dev/null +++ b/engine/render/pass/shadow.go @@ -0,0 +1,206 @@ +package pass + +import ( + "fmt" + "github.com/vkngwrapper/core/v2/core1_0" + "log" + "zworld/engine/object" + "zworld/engine/object/light" + "zworld/engine/object/mesh" + "zworld/engine/renderapi" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/framebuffer" + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/renderpass/attachment" + "zworld/engine/renderapi/texture" + "zworld/engine/renderapi/vulkan" +) + +type Shadow interface { + Pass + + Shadowmap(lit light.T, cascade int) texture.T +} + +type shadowpass struct { + app vulkan.App + target vulkan.Target + pass renderpass.T + size int + + // should be replaced with a proper cache that will evict unused maps + shadowmaps map[light.T]Shadowmap + + lightQuery *object.Query[light.T] + meshQuery *object.Query[mesh.Mesh] +} + +type Shadowmap struct { + Cascades []Cascade +} + +type Cascade struct { + Texture texture.T + Frame framebuffer.T + Mats MaterialCache +} + +func NewShadowPass(app vulkan.App, target vulkan.Target) Shadow { + pass := renderpass.New(app.Device(), renderpass.Args{ + Name: "Shadow", + DepthAttachment: &attachment.Depth{ + Image: attachment.NewImage("shadowmap", core1_0.FormatD32SignedFloat, core1_0.ImageUsageDepthStencilAttachment|core1_0.ImageUsageInputAttachment|core1_0.ImageUsageSampled), + LoadOp: core1_0.AttachmentLoadOpClear, + StencilLoadOp: core1_0.AttachmentLoadOpClear, + StoreOp: core1_0.AttachmentStoreOpStore, + FinalLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + ClearDepth: 1, + }, + Subpasses: []renderpass.Subpass{ + { + Name: MainSubpass, + Depth: true, + }, + }, + Dependencies: []renderpass.SubpassDependency{ + { + Src: renderpass.ExternalSubpass, + Dst: MainSubpass, + Flags: core1_0.DependencyByRegion, + + // External passes must finish reading depth textures in fragment shaders + SrcStageMask: core1_0.PipelineStageEarlyFragmentTests | core1_0.PipelineStageLateFragmentTests, + SrcAccessMask: core1_0.AccessDepthStencilAttachmentRead, + + // Before we can write to the depth buffer + DstStageMask: core1_0.PipelineStageEarlyFragmentTests | core1_0.PipelineStageLateFragmentTests, + DstAccessMask: core1_0.AccessDepthStencilAttachmentWrite, + }, + { + Src: MainSubpass, + Dst: renderpass.ExternalSubpass, + Flags: core1_0.DependencyByRegion, + + // The shadow pass must finish writing the depth attachment + SrcStageMask: core1_0.PipelineStageEarlyFragmentTests | core1_0.PipelineStageLateFragmentTests, + SrcAccessMask: core1_0.AccessDepthStencilAttachmentWrite, + + // Before it can be used as a shadow map texture in a fragment shader + DstStageMask: core1_0.PipelineStageFragmentShader, + DstAccessMask: core1_0.AccessShaderRead, + }, + }, + }) + + return &shadowpass{ + app: app, + target: target, + pass: pass, + shadowmaps: make(map[light.T]Shadowmap), + size: 2048, + + meshQuery: object.NewQuery[mesh.Mesh](), + lightQuery: object.NewQuery[light.T](), + } +} + +func (p *shadowpass) Name() string { + return "Shadow" +} + +func (p *shadowpass) createShadowmap(light light.T) Shadowmap { + log.Println("creating shadowmap for", light.Name()) + + cascades := make([]Cascade, light.Shadowmaps()) + for i := range cascades { + key := fmt.Sprintf("%s-%d", object.Key("light", light), i) + fbuf, err := framebuffer.New(p.app.Device(), key, p.size, p.size, p.pass) + if err != nil { + panic(err) + } + + // the frame buffer object will allocate a new depth image for us + view := fbuf.Attachment(attachment.DepthName) + tex, err := texture.FromView(p.app.Device(), key, view, texture.Args{ + Aspect: core1_0.ImageAspectDepth, + }) + if err != nil { + panic(err) + } + + cascades[i].Texture = tex + cascades[i].Frame = fbuf + + // each light cascade needs its own shadow materials - or rather, their own descriptors + // cheating a bit by creating entire materials for each light, fix it later. + mats := NewShadowMaterialMaker(p.app, p.pass, p.target.Frames()) + cascades[i].Mats = mats + } + + shadowmap := Shadowmap{ + Cascades: cascades, + } + p.shadowmaps[light] = shadowmap + return shadowmap +} + +func (p *shadowpass) Record(cmds command.Recorder, args renderapi.Args, scene object.Component) { + lights := p.lightQuery. + Reset(). + Where(func(lit light.T) bool { return lit.Type() == light.TypeDirectional && lit.CastShadows() }). + Collect(scene) + + for _, light := range lights { + shadowmap, mapExists := p.shadowmaps[light] + if !mapExists { + shadowmap = p.createShadowmap(light) + } + + for index, cascade := range shadowmap.Cascades { + camera := light.ShadowProjection(index) + + frame := cascade.Frame + cmds.Record(func(cmd command.Buffer) { + cmd.CmdBeginRenderPass(p.pass, frame) + }) + + // todo: filter only meshes that cast shadows + meshes := p.meshQuery. + Reset(). + Where(castsShadows). + Collect(scene) + + groups := MaterialGroups(cascade.Mats, args.Frame, meshes) + groups.Draw(cmds, camera, nil) + + cmds.Record(func(cmd command.Buffer) { + cmd.CmdEndRenderPass() + }) + } + } +} + +func castsShadows(m mesh.Mesh) bool { + return m.CastShadows() +} + +func (p *shadowpass) Shadowmap(light light.T, cascade int) texture.T { + if shadowmap, exists := p.shadowmaps[light]; exists { + return shadowmap.Cascades[cascade].Texture + } + return nil +} + +func (p *shadowpass) Destroy() { + for _, shadowmap := range p.shadowmaps { + for _, cascade := range shadowmap.Cascades { + cascade.Frame.Destroy() + cascade.Texture.Destroy() + cascade.Mats.Destroy() + } + } + p.shadowmaps = nil + + p.pass.Destroy() + p.pass = nil +} diff --git a/engine/render/pass/shadow_cache.go b/engine/render/pass/shadow_cache.go new file mode 100644 index 0000000..2d7083f --- /dev/null +++ b/engine/render/pass/shadow_cache.go @@ -0,0 +1,36 @@ +package pass + +import ( + "zworld/engine/object/light" + "zworld/engine/renderapi/cache" +) + +type ShadowCache struct { + samplers cache.SamplerCache + lookup ShadowmapLookupFn + shared bool +} + +var _ light.ShadowmapStore = &ShadowCache{} + +func NewShadowCache(samplers cache.SamplerCache, lookup ShadowmapLookupFn) *ShadowCache { + return &ShadowCache{ + samplers: samplers, + lookup: lookup, + shared: true, + } +} + +func (s *ShadowCache) Lookup(lit light.T, cascade int) (int, bool) { + if shadowtex := s.lookup(lit, cascade); shadowtex != nil { + handle := s.samplers.Assign(shadowtex) + return handle.ID, true + } + // no shadowmap available + return 0, false +} + +// Flush the underlying sampler cache +func (s *ShadowCache) Flush() { + s.samplers.Flush() +} diff --git a/engine/render/pass/shadow_mat_cache.go b/engine/render/pass/shadow_mat_cache.go new file mode 100644 index 0000000..0ca7a1b --- /dev/null +++ b/engine/render/pass/shadow_mat_cache.go @@ -0,0 +1,88 @@ +package pass + +import ( + "github.com/vkngwrapper/core/v2/core1_0" + "zworld/engine/render/uniform" + "zworld/engine/renderapi/cache" + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/shader" + "zworld/engine/renderapi/vertex" + "zworld/engine/renderapi/vulkan" +) + +type ShadowMatCache struct { + app vulkan.App + pass renderpass.T + frames int +} + +func NewShadowMaterialMaker(app vulkan.App, pass renderpass.T, frames int) MaterialCache { + return cache.New[*material.Def, []Material](&ShadowMatCache{ + app: app, + pass: pass, + frames: frames, + }) +} + +func (m *ShadowMatCache) Name() string { return "ShadowMaterials" } + +func (m *ShadowMatCache) Instantiate(def *material.Def, callback func([]Material)) { + if def == nil { + def = &material.Def{} + } + + desc := &BasicDescriptors{ + Camera: &descriptor.Uniform[uniform.Camera]{ + Stages: core1_0.StageAll, + }, + Objects: &descriptor.Storage[uniform.Object]{ + Stages: core1_0.StageAll, + Size: 2000, + }, + } + + // read vertex pointers from vertex format + pointers := vertex.ParsePointers(def.VertexFormat) + + // fetch shader from cache + shader := m.app.Shaders().Fetch(shader.NewRef("shadow")) + + // create material + mat := material.New( + m.app.Device(), + material.Args{ + Shader: shader, + Pass: m.pass, + Subpass: MainSubpass, + Pointers: pointers, + CullMode: vertex.CullFront, + DepthTest: true, + DepthWrite: true, + DepthFunc: core1_0.CompareOpLess, + DepthClamp: true, + Primitive: def.Primitive, + }, + desc) + + instances := make([]Material, m.frames) + for i := range instances { + instance := mat.Instantiate(m.app.Pool()) + instances[i] = &BasicMaterial{ + id: def.Hash(), + Instance: instance, + Objects: NewObjectBuffer(desc.Objects.Size), + Meshes: m.app.Meshes(), + } + } + + callback(instances) +} + +func (m *ShadowMatCache) Destroy() { +} + +func (m *ShadowMatCache) Delete(mat []Material) { + mat[0].Destroy() +} diff --git a/engine/render/pass/ssao.go b/engine/render/pass/ssao.go new file mode 100644 index 0000000..299aef2 --- /dev/null +++ b/engine/render/pass/ssao.go @@ -0,0 +1,267 @@ +package pass + +import ( + "fmt" + "github.com/vkngwrapper/core/v2/core1_0" + "unsafe" + "zworld/engine/object" + "zworld/engine/renderapi" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/framebuffer" + "zworld/engine/renderapi/image" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/renderpass/attachment" + "zworld/engine/renderapi/shader" + "zworld/engine/renderapi/texture" + "zworld/engine/renderapi/vertex" + "zworld/engine/renderapi/vulkan" + "zworld/plugins/math" + "zworld/plugins/math/mat4" + "zworld/plugins/math/random" + "zworld/plugins/math/vec3" + "zworld/plugins/math/vec4" +) + +const SSAOSamples = 32 + +type AmbientOcclusionPass struct { + app vulkan.App + pass renderpass.T + fbuf framebuffer.Array + mat *material.Material[*AmbientOcclusionDescriptors] + desc []*material.Instance[*AmbientOcclusionDescriptors] + quad vertex.Mesh + + scale float32 + position []texture.T + normal []texture.T + kernel [SSAOSamples]vec4.T + noise *HemisphereNoise +} + +var _ Pass = &AmbientOcclusionPass{} + +type AmbientOcclusionParams struct { + Projection mat4.T + Kernel [SSAOSamples]vec4.T + Samples int32 + Scale float32 + Radius float32 + Bias float32 + Power float32 +} + +type AmbientOcclusionDescriptors struct { + descriptor.Set + Position *descriptor.Sampler + Normal *descriptor.Sampler + Noise *descriptor.Sampler + Params *descriptor.Uniform[AmbientOcclusionParams] +} + +func NewAmbientOcclusionPass(app vulkan.App, target vulkan.Target, gbuffer GeometryBuffer) *AmbientOcclusionPass { + var err error + p := &AmbientOcclusionPass{ + app: app, + scale: float32(gbuffer.Width()) / float32(target.Width()), + } + + p.pass = renderpass.New(app.Device(), renderpass.Args{ + Name: "AmbientOcclusion", + ColorAttachments: []attachment.Color{ + { + Name: OutputAttachment, + Image: attachment.FromImageArray(target.Surfaces()), + LoadOp: core1_0.AttachmentLoadOpDontCare, + StoreOp: core1_0.AttachmentStoreOpStore, + FinalLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + }, + }, + Subpasses: []renderpass.Subpass{ + { + Name: MainSubpass, + Depth: false, + + ColorAttachments: []attachment.Name{OutputAttachment}, + }, + }, + }) + + p.mat = material.New( + app.Device(), + material.Args{ + Shader: app.Shaders().Fetch(shader.NewRef("ssao")), + Pass: p.pass, + Pointers: vertex.ParsePointers(vertex.T{}), + DepthTest: false, + DepthWrite: false, + }, + &AmbientOcclusionDescriptors{ + Position: &descriptor.Sampler{ + Stages: core1_0.StageFragment, + }, + Normal: &descriptor.Sampler{ + Stages: core1_0.StageFragment, + }, + Noise: &descriptor.Sampler{ + Stages: core1_0.StageFragment, + }, + Params: &descriptor.Uniform[AmbientOcclusionParams]{ + Stages: core1_0.StageFragment, + }, + }) + + p.fbuf, err = framebuffer.NewArray(target.Frames(), app.Device(), "ssao", target.Width(), target.Height(), p.pass) + if err != nil { + panic(err) + } + + p.quad = vertex.ScreenQuad("ssao-pass-quad") + + // create noise texture + p.noise = NewHemisphereNoise(4, 4) + + // create sampler kernel + p.kernel = [SSAOSamples]vec4.T{} + for i := 0; i < len(p.kernel); i++ { + var sample vec3.T + for { + sample = vec3.Random( + vec3.New(-1, 0, -1), + vec3.New(1, 1, 1), + ) + if sample.LengthSqr() > 1 { + continue + } + sample = sample.Normalized() + if vec3.Dot(sample, vec3.Up) < 0.5 { + continue + } + + sample = sample.Scaled(random.Range(0, 1)) + break + } + + // we dont want a uniform sample distribution + // push samples closer to the origin + scale := float32(i) / float32(SSAOSamples) + scale = math.Lerp(0.1, 1.0, scale*scale) + sample = sample.Scaled(scale) + + p.kernel[i] = vec4.Extend(sample, 0) + } + + // todo: if we shuffle the kernel, it would be ok to use fewer samples + + p.desc = p.mat.InstantiateMany(app.Pool(), target.Frames()) + p.position = make([]texture.T, target.Frames()) + p.normal = make([]texture.T, target.Frames()) + for i := 0; i < target.Frames(); i++ { + posKey := fmt.Sprintf("ssao-position-%d", i) + p.position[i], err = texture.FromImage(app.Device(), posKey, gbuffer.Position()[i], texture.Args{ + Filter: texture.FilterNearest, + Wrap: texture.WrapClamp, + }) + if err != nil { + // todo: clean up + panic(err) + } + p.desc[i].Descriptors().Position.Set(p.position[i]) + + normKey := fmt.Sprintf("ssao-normal-%d", i) + p.normal[i], err = texture.FromImage(app.Device(), normKey, gbuffer.Normal()[i], texture.Args{ + Filter: texture.FilterNearest, + Wrap: texture.WrapClamp, + }) + if err != nil { + // todo: clean up + panic(err) + } + p.desc[i].Descriptors().Normal.Set(p.normal[i]) + } + + return p +} + +func (p *AmbientOcclusionPass) Record(cmds command.Recorder, args renderapi.Args, scene object.Component) { + quad := p.app.Meshes().Fetch(p.quad) + + cmds.Record(func(cmd command.Buffer) { + cmd.CmdBeginRenderPass(p.pass, p.fbuf[args.Frame]) + p.desc[args.Frame].Bind(cmd) + p.desc[args.Frame].Descriptors().Noise.Set(p.app.Textures().Fetch(p.noise)) + p.desc[args.Frame].Descriptors().Params.Set(AmbientOcclusionParams{ + Projection: args.Projection, + Kernel: p.kernel, + Samples: 32, + Scale: p.scale, + Radius: 0.4, + Bias: 0.02, + Power: 2.6, + }) + quad.Draw(cmd, 0) + cmd.CmdEndRenderPass() + }) +} + +func (p *AmbientOcclusionPass) Destroy() { + p.pass.Destroy() + p.fbuf.Destroy() + for i := 0; i < len(p.position); i++ { + p.position[i].Destroy() + p.normal[i].Destroy() + } + p.mat.Destroy() +} + +func (p *AmbientOcclusionPass) Name() string { + return "AmbientOcclusion" +} + +type HemisphereNoise struct { + Width int + Height int + + key string +} + +func NewHemisphereNoise(width, height int) *HemisphereNoise { + return &HemisphereNoise{ + key: fmt.Sprintf("noise-hemisphere-%dx%d", width, height), + Width: width, + Height: height, + } +} + +func (n *HemisphereNoise) Key() string { return n.key } +func (n *HemisphereNoise) Version() int { return 1 } + +func (n *HemisphereNoise) ImageData() *image.Data { + buffer := make([]vec4.T, 4*n.Width*n.Height) + for i := range buffer { + buffer[i] = vec4.Extend(vec3.Random( + vec3.New(-1, -1, 0), + vec3.New(1, 1, 0), + ).Normalized(), 0) + } + + // cast to byte array + ptr := (*byte)(unsafe.Pointer(&buffer[0])) + bytes := unsafe.Slice(ptr, int(unsafe.Sizeof(vec4.T{}))*len(buffer)) + + return &image.Data{ + Width: n.Width, + Height: n.Height, + Format: core1_0.FormatR32G32B32A32SignedFloat, + Buffer: bytes, + } +} + +func (n *HemisphereNoise) TextureArgs() texture.Args { + return texture.Args{ + Filter: texture.FilterNearest, + Wrap: texture.WrapRepeat, + } +} diff --git a/engine/render/uniform/camera.go b/engine/render/uniform/camera.go new file mode 100644 index 0000000..daafcaa --- /dev/null +++ b/engine/render/uniform/camera.go @@ -0,0 +1,19 @@ +package uniform + +import ( + "zworld/plugins/math/mat4" + "zworld/plugins/math/vec2" + "zworld/plugins/math/vec4" +) + +type Camera struct { + Proj mat4.T + View mat4.T + ViewProj mat4.T + ProjInv mat4.T + ViewInv mat4.T + ViewProjInv mat4.T + Eye vec4.T + Forward vec4.T + Viewport vec2.T +} diff --git a/engine/render/uniform/light.go b/engine/render/uniform/light.go new file mode 100644 index 0000000..f97a959 --- /dev/null +++ b/engine/render/uniform/light.go @@ -0,0 +1,41 @@ +package uniform + +import ( + "fmt" + "unsafe" + "zworld/engine/renderapi/color" + "zworld/plugins/math/mat4" + "zworld/plugins/math/vec4" +) + +type Light struct { + ViewProj [4]mat4.T + Shadowmap [4]uint32 + Distance [4]float32 + Color color.T + Position vec4.T + Type uint32 + Intensity float32 + Range float32 + Falloff float32 + _padding float32 +} + +type LightSettings struct { + AmbientColor color.T + AmbientIntensity float32 + Count int32 + ShadowSamples int32 + ShadowSampleRadius float32 + ShadowBias float32 + NormalOffset float32 + _padding [75]float32 +} + +func init() { + lightSz := unsafe.Sizeof(Light{}) + settingSz := unsafe.Sizeof(LightSettings{}) + if lightSz != settingSz { + panic(fmt.Sprintf("Light (%d) and LightSetting (%d) must have equal size", lightSz, settingSz)) + } +} diff --git a/engine/render/uniform/object.go b/engine/render/uniform/object.go new file mode 100644 index 0000000..b251b03 --- /dev/null +++ b/engine/render/uniform/object.go @@ -0,0 +1,8 @@ +package uniform + +import "zworld/plugins/math/mat4" + +type Object struct { + Model mat4.T + Textures [4]uint32 +} diff --git a/engine/renderapi/args.go b/engine/renderapi/args.go new file mode 100644 index 0000000..a3e3332 --- /dev/null +++ b/engine/renderapi/args.go @@ -0,0 +1,58 @@ +package renderapi + +import ( + "zworld/engine/renderapi/color" + "zworld/plugins/math/mat4" + "zworld/plugins/math/transform" + "zworld/plugins/math/vec2" + "zworld/plugins/math/vec3" +) + +// Args holds the arguments used to perform a draw pass. +// Includes the various transformation matrices and position of the camera. +type Args struct { + Frame int + Time float32 + Delta float32 + VP mat4.T + VPInv mat4.T + MVP mat4.T + Projection mat4.T + View mat4.T + ViewInv mat4.T + Transform mat4.T + Position vec3.T + Forward vec3.T + Fov float32 + Near float32 + Far float32 + Viewport Screen + Clear color.T +} + +type Screen struct { + Width int + Height int + Scale float32 +} + +func (s Screen) Size() vec2.T { + return vec2.NewI(s.Width, s.Height) +} + +func (s Screen) NormalizeCursor(cursor vec2.T) vec2.T { + return cursor.Div(s.Size()).Sub(vec2.New(0.5, 0.5)).Scaled(2) +} + +// Apply the effects of a transform +func (d Args) Apply(t mat4.T) Args { + d.Transform = d.Transform.Mul(&t) + d.MVP = d.VP.Mul(&d.Transform) + return d +} + +func (d Args) Set(t transform.T) Args { + d.Transform = t.Matrix() + d.MVP = d.VP.Mul(&d.Transform) + return d +} diff --git a/engine/renderapi/buffer/array.go b/engine/renderapi/buffer/array.go new file mode 100644 index 0000000..7d9dd1c --- /dev/null +++ b/engine/renderapi/buffer/array.go @@ -0,0 +1,73 @@ +package buffer + +import ( + "fmt" + "reflect" + + "zworld/engine/renderapi/device" + "zworld/engine/util" +) + +// Strongly typed array buffer +type Array[K any] interface { + T + + // Set the value of element i and flushes the buffer. + Set(index int, data K) + + // Sets a range of elements, starting at i and flushes the buffer. + SetRange(index int, data []K) + + // Count returns the number of items in the array + Count() int + + // Element returns the aligned byte size of a single element + Element() int +} + +type array[K any] struct { + T + element int + count int +} + +// NewArray creates a new typed array buffer. +// When allocating arrays, the Size argument is the number of elements +func NewArray[K any](device device.T, args Args) Array[K] { + align, maxSize := GetBufferLimits(device, args.Usage) + + var empty K + kind := reflect.TypeOf(empty) + + element := util.Align(int(kind.Size()), align) + + count := args.Size + size := count * element + if size > maxSize { + panic(fmt.Sprintf("buffer is too large for the specified usage. size: %d, max: %d", size, maxSize)) + } + + args.Size = size + buffer := New(device, args) + + return &array[K]{ + T: buffer, + element: element, + count: count, + } +} + +func (a *array[K]) Set(index int, data K) { + a.Write(index*a.element, &data) + a.Flush() +} + +func (a *array[K]) SetRange(offset int, data []K) { + for i, el := range data { + a.Write((i+offset)*a.element, &el) + } + a.Flush() +} + +func (a *array[K]) Count() int { return a.count } +func (a *array[K]) Element() int { return a.element } diff --git a/engine/renderapi/buffer/buffer.go b/engine/renderapi/buffer/buffer.go new file mode 100644 index 0000000..57ed2fe --- /dev/null +++ b/engine/renderapi/buffer/buffer.go @@ -0,0 +1,125 @@ +package buffer + +import ( + "zworld/engine/renderapi/device" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/core/v2/driver" +) + +type T interface { + device.Resource[core1_0.Buffer] + + // Size returns the total allocation size of the buffer in bytes + Size() int + + // Read directly from the buffer at the given offset + Read(offset int, data any) int + + // Write directly to the buffer at the given offset + Write(offset int, data any) int + + Flush() + + // Memory returns a handle to the underlying memory block + Memory() device.Memory +} + +type Args struct { + Key string + Size int + Usage core1_0.BufferUsageFlags + Memory core1_0.MemoryPropertyFlags +} + +type buffer struct { + ptr core1_0.Buffer + device device.T + memory device.Memory + size int +} + +func New(device device.T, args Args) T { + if args.Size == 0 { + panic("buffer size cant be 0") + } + + queueIdx := device.GetQueueFamilyIndex(core1_0.QueueGraphics) + ptr, _, err := device.Ptr().CreateBuffer(nil, core1_0.BufferCreateInfo{ + Flags: 0, + Size: args.Size, + Usage: args.Usage, + SharingMode: core1_0.SharingModeExclusive, + QueueFamilyIndices: []int{queueIdx}, + }) + if err != nil { + panic(err) + } + + if args.Key != "" { + device.SetDebugObjectName(driver.VulkanHandle(ptr.Handle()), + core1_0.ObjectTypeBuffer, args.Key) + } + + memreq := ptr.MemoryRequirements() + + mem := device.Allocate(args.Key, *memreq, args.Memory) + ptr.BindBufferMemory(mem.Ptr(), 0) + + return &buffer{ + ptr: ptr, + device: device, + memory: mem, + size: int(memreq.Size), + } +} + +func NewShared(device device.T, key string, size int) T { + return New(device, Args{ + Key: key, + Size: size, + Usage: core1_0.BufferUsageTransferSrc | core1_0.BufferUsageTransferDst, + Memory: core1_0.MemoryPropertyHostVisible | core1_0.MemoryPropertyHostCoherent, + }) +} + +func NewRemote(device device.T, key string, size int, flags core1_0.BufferUsageFlags) T { + return New(device, Args{ + Key: key, + Size: size, + Usage: core1_0.BufferUsageTransferDst | flags, + Memory: core1_0.MemoryPropertyDeviceLocal, + }) +} + +func (b *buffer) Ptr() core1_0.Buffer { + return b.ptr +} + +func (b *buffer) Size() int { + return b.size +} + +func (b *buffer) Memory() device.Memory { + return b.memory +} + +func (b *buffer) Destroy() { + b.ptr.Destroy(nil) + b.memory.Destroy() + b.ptr = nil + b.memory = nil + b.device = nil +} + +func (b *buffer) Write(offset int, data any) int { + return b.memory.Write(offset, data) +} + +func (b *buffer) Read(offset int, data any) int { + return b.memory.Read(offset, data) +} + +func (b *buffer) Flush() { + b.memory.Flush() +} diff --git a/engine/renderapi/buffer/item.go b/engine/renderapi/buffer/item.go new file mode 100644 index 0000000..9affa6c --- /dev/null +++ b/engine/renderapi/buffer/item.go @@ -0,0 +1,47 @@ +package buffer + +import ( + "fmt" + "reflect" + + "zworld/engine/renderapi/device" + "zworld/engine/util" +) + +type Item[K any] interface { + T + + // Set the data in the buffer and flushes. + Set(data K) +} + +type item[K any] struct { + T +} + +// NewItem creates a new typed single-item buffer. +// When allocating items, the Size argument is ignored +func NewItem[K any](device device.T, args Args) Item[K] { + align, maxSize := GetBufferLimits(device, args.Usage) + + var empty K + kind := reflect.TypeOf(empty) + + element := util.Align(int(kind.Size()), align) + if element > maxSize { + panic(fmt.Sprintf("buffer is too large for the specified usage. size: %d, max: %d", element, maxSize)) + } + + args.Size = element + buffer := New(device, args) + + return &item[K]{ + T: buffer, + } +} + +func (i *item[K]) Set(data K) { + ptr := &data + i.Write(0, ptr) + i.Flush() +} diff --git a/engine/renderapi/buffer/util.go b/engine/renderapi/buffer/util.go new file mode 100644 index 0000000..cbc782d --- /dev/null +++ b/engine/renderapi/buffer/util.go @@ -0,0 +1,18 @@ +package buffer + +import ( + "zworld/engine/renderapi/device" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +func GetBufferLimits(device device.T, usage core1_0.BufferUsageFlags) (align, max int) { + limits := device.GetLimits() + if usage&core1_0.BufferUsageUniformBuffer > 0 { + return int(limits.MinUniformBufferOffsetAlignment), int(limits.MaxUniformBufferRange) + } + if usage&core1_0.BufferUsageStorageBuffer > 0 { + return int(limits.MinStorageBufferOffsetAlignment), int(limits.MaxStorageBufferRange) + } + panic("unknown buffer usage type") +} diff --git a/engine/renderapi/cache/allocator/allocator.go b/engine/renderapi/cache/allocator/allocator.go new file mode 100644 index 0000000..ca9e25b --- /dev/null +++ b/engine/renderapi/cache/allocator/allocator.go @@ -0,0 +1,177 @@ +package allocator + +import ( + "errors" + "math" +) + +// Minimum allocation block size +const MinAlloc = 256 + +var minBucketTier = int(math.Log2(MinAlloc)) + +var ErrOutOfMemory = errors.New("out of memory") +var ErrInvalidFree = errors.New("illegal free() call") + +type Block struct { + Offset int + Size int +} + +type T interface { + Alloc(int) (Block, error) + Free(int) error +} + +// Buddy allocator implementation +type buddy struct { + free [][]Block + alloc map[int]int + top int +} + +func New(size int) T { + if !IsPowerOfTwo(size) { + panic("allocator size must be a power of 2") + } + + top := GetBucketTier(size) + free := make([][]Block, top+1) + free[top] = []Block{{Offset: 0, Size: size}} + + return &buddy{ + top: top, + free: free, + alloc: map[int]int{}, + } +} + +func (f *buddy) Alloc(size int) (Block, error) { + tier := GetBucketTier(size) + block, err := f.getBlock(tier) + if err != nil { + return Block{}, err + } + f.alloc[block.Offset] = block.Size + return block, nil +} + +func (f *buddy) getBlock(tier int) (Block, error) { + if tier > f.top { + return Block{}, ErrOutOfMemory + } + + if bucket := f.free[tier]; len(bucket) > 0 { + lastIdx := len(bucket) - 1 + block := bucket[lastIdx] + f.free[tier] = bucket[:lastIdx] + return block, nil + } + + split, err := f.getBlock(tier + 1) + if err != nil { + return Block{}, err + } + + size := split.Size / 2 + f.free[tier] = append(f.free[tier], Block{ + Offset: split.Offset + size, + Size: size, + }) + return Block{ + Offset: split.Offset, + Size: size, + }, nil +} + +func (f *buddy) Free(offset int) error { + size, exists := f.alloc[offset] + if !exists { + return ErrInvalidFree + } + + freed := Block{ + Offset: offset, + Size: size, + } + + tier := GetBucketTier(size) + f.free[tier] = append(f.free[tier], freed) + + // mark as free + delete(f.alloc, offset) + + // merge buddies + f.merge(tier, freed, len(f.free[tier])-1) + + return nil +} + +func (f *buddy) merge(tier int, block Block, blockIdx int) { + // nothing to merge at the top tier + if tier >= f.top { + return + } + level := f.free[tier] + + // figure out the offset of our buddy block, and the resulting offset of a merge + buddyOffset := 0 + mergedOffset := 0 + if block.Offset%(2*block.Size) == 0 { + // we are an even block, buddy is after + buddyOffset = block.Offset + block.Size + mergedOffset = block.Offset + } else { + // we are an odd block, buddy is before + buddyOffset = block.Offset - block.Size + mergedOffset = buddyOffset + } + + // check if buddy block is allocated + if _, allocated := f.alloc[buddyOffset]; allocated { + // yes - then we can't merge + return + } + + // find the free list index of the buddy + var buddyIdx int + for candidateIdx, candidate := range level { + if candidate.Offset == buddyOffset { + buddyIdx = candidateIdx + break + } + } + + // remove both blocks from free list + // todo: implement using a linked list + if buddyIdx > blockIdx { + f.free[tier] = append(append(level[:blockIdx], level[blockIdx+1:buddyIdx]...), level[buddyIdx+1:]...) + } else { + f.free[tier] = append(append(level[:buddyIdx], level[buddyIdx+1:blockIdx]...), level[blockIdx+1:]...) + } + + // add the merged block to free list, on the next tier + merged := Block{ + Offset: mergedOffset, + Size: 2 * block.Size, + } + f.free[tier+1] = append(f.free[tier+1], merged) + + // attempt to merge next level + f.merge(tier+1, merged, len(f.free[tier+1])-1) +} + +func GetBucketTier(size int) int { + tier := int(math.Log2(float64(size-1))) + 1 + + tier -= minBucketTier + if tier < 0 { + return 0 + } + + return tier +} + +func IsPowerOfTwo(n int) bool { + return n > 0 && (n&(n-1)) == 0 +} diff --git a/engine/renderapi/cache/allocator/allocator_test.go b/engine/renderapi/cache/allocator/allocator_test.go new file mode 100644 index 0000000..66cde01 --- /dev/null +++ b/engine/renderapi/cache/allocator/allocator_test.go @@ -0,0 +1,49 @@ +package allocator_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "zworld/engine/renderapi/cache/allocator" +) + +var _ = Describe("", func() { + It("allocates!", func() { + fl := allocator.New(1024) + block, err := fl.Alloc(16) + Expect(err).ToNot(HaveOccurred()) + Expect(block.Size).To(Equal(256)) + + err = fl.Free(block.Offset) + Expect(err).ToNot(HaveOccurred()) + + block2, err := fl.Alloc(257) + Expect(err).ToNot(HaveOccurred()) + Expect(block2.Size).To(Equal(512)) + }) + + It("allocates correct sizes", func() { + fl := allocator.New(1024) + block, err := fl.Alloc(257) + Expect(err).ToNot(HaveOccurred()) + Expect(block.Size).To(Equal(512)) + }) + + It("assigns tiers correctly", func() { + Expect(allocator.GetBucketTier(allocator.MinAlloc)).To(Equal(0)) + Expect(allocator.GetBucketTier(allocator.MinAlloc + 1)).To(Equal(1)) + Expect(allocator.GetBucketTier(2 * allocator.MinAlloc)).To(Equal(1)) + Expect(allocator.GetBucketTier(2*allocator.MinAlloc + 1)).To(Equal(2)) + }) + + It("checks powers of two", func() { + Expect(allocator.IsPowerOfTwo(2)).To(BeTrue()) + Expect(allocator.IsPowerOfTwo(4)).To(BeTrue()) + Expect(allocator.IsPowerOfTwo(8)).To(BeTrue()) + Expect(allocator.IsPowerOfTwo(16)).To(BeTrue()) + + Expect(allocator.IsPowerOfTwo(0)).To(BeFalse()) + Expect(allocator.IsPowerOfTwo(3)).To(BeFalse()) + Expect(allocator.IsPowerOfTwo(121)).To(BeFalse()) + }) +}) diff --git a/engine/renderapi/cache/allocator/suite_test.go b/engine/renderapi/cache/allocator/suite_test.go new file mode 100644 index 0000000..157958d --- /dev/null +++ b/engine/renderapi/cache/allocator/suite_test.go @@ -0,0 +1,13 @@ +package allocator_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestAllocator(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "renderapi/cache/allocator") +} diff --git a/engine/renderapi/cache/cache.go b/engine/renderapi/cache/cache.go new file mode 100644 index 0000000..d3261be --- /dev/null +++ b/engine/renderapi/cache/cache.go @@ -0,0 +1,210 @@ +package cache + +import ( + "sync" + + "zworld/engine/renderapi/command" + "zworld/engine/util" +) + +type T[K Key, V Value] interface { + // TryFetch returns a value if it exists and is ready to use. + // Resets the age of the cache line + // Returns a bool indicating whether the value exists. + TryFetch(K) (V, bool) + + // Fetch returns a value, waiting until its becomes available if it does not yet exist + // Resets the age of the cache line + Fetch(K) V + + // MaxAge returns the number of ticks until unused lines are evicted + MaxAge() int + + // Tick increments the age of all cache lines, and evicts those + // that have not been accessed in maxAge ticks or more. + Tick() + + // Destroy the cache and all data held in it. + Destroy() +} + +type Key interface { + Key() string + Version() int +} + +type Value interface{} + +type Backend[K Key, V Value] interface { + // Instantiate the resource referred to by Key. + // Must execute on a background goroutine + Instantiate(K, func(V)) + + Delete(V) + Destroy() + Name() string +} + +type line[V Value] struct { + value V + age int + version int + available bool + wait chan struct{} +} + +type cache[K Key, V Value] struct { + backend Backend[K, V] + data map[string]*line[V] + worker *command.ThreadWorker + lock *sync.RWMutex + maxAge int + async bool +} + +func New[K Key, V Value](backend Backend[K, V]) T[K, V] { + c := &cache[K, V]{ + backend: backend, + data: map[string]*line[V]{}, + worker: command.NewThreadWorker(backend.Name(), 100, false), + lock: &sync.RWMutex{}, + maxAge: 100, + async: false, + } + return c +} + +func (c cache[K, V]) MaxAge() int { return c.maxAge } + +func (c *cache[K, V]) get(key K) (*line[V], bool) { + c.lock.RLock() + ln, hit := c.data[key.Key()] + c.lock.RUnlock() + return ln, hit +} + +func (c *cache[K, V]) init(key K) *line[V] { + ln := &line[V]{ + available: false, + wait: make(chan struct{}), + } + c.lock.Lock() + c.data[key.Key()] = ln + c.lock.Unlock() + return ln +} + +func (c *cache[K, V]) fetch(key K) *line[V] { + ln, hit := c.get(key) + if !hit { + ln = c.init(key) + } + + // check if a newer version has been requested + // since the initial line has version 0, this always happens on the first request. + if ln.version != key.Version() { + // update version immediately, so that duplicate instantiantions wont happen. + // note that the previous version will be returned until the new one is available + ln.version = key.Version() + + // instantiate new version + c.backend.Instantiate(key, func(value V) { + if ln.available { + // available implies that we have a previous value + // however, it is most likely in use rendering the in-flight frame! + // deleting it here may cause a segfault + c.deleteLater(ln.value) + } + ln.value = value + + // if its the very first time this item is requested, signal any waiting + // synchronous fetch that it is ready. + if !ln.available { + ln.available = true + close(ln.wait) + } + }) + } + + // reset age + ln.age = 0 + + return ln +} + +func (c *cache[K, V]) TryFetch(key K) (V, bool) { + if !c.async { + return c.Fetch(key), true + } + + ln := c.fetch(key) + + // not available yet - return nothing + if !ln.available { + var empty V + return empty, false + } + + return ln.value, true +} + +func (c *cache[K, V]) Fetch(key K) V { + ln := c.fetch(key) + + // not available yet - wait for it. + if !ln.available { + <-ln.wait + } + + return ln.value +} + +func (c *cache[K, V]) deleteLater(value V) { + // we can reuse the eviction mechanic to delete values later + // simply attach it to a cache line with a random key that will never be accessed + c.lock.Lock() + defer c.lock.Unlock() + randomKey := "trash-" + util.NewUUID(8) + c.data[randomKey] = &line[V]{ + value: value, + available: true, + age: c.maxAge - 10, // delete in 10 frames + } +} + +func (c *cache[K, V]) Tick() { + // eviction + c.lock.Lock() + defer c.lock.Unlock() + for key, line := range c.data { + line.age++ + if line.age > c.maxAge { + delete(c.data, key) + + // delete any instantiated object + if line.available { + c.backend.Delete(line.value) + } + } + } +} + +func (c *cache[K, V]) Destroy() { + c.lock.Lock() + defer c.lock.Unlock() + + // flush any pending work + c.worker.Flush() + + // destroy all the data in the cache + for _, line := range c.data { + // if the cache line is pending creation, we must wait for it to complete + // before destroying it. failing to do so may cause a segfault + if !line.available { + <-line.wait + } + c.backend.Delete(line.value) + } + + c.backend.Destroy() +} diff --git a/engine/renderapi/cache/mesh.go b/engine/renderapi/cache/mesh.go new file mode 100644 index 0000000..9a43d57 --- /dev/null +++ b/engine/renderapi/cache/mesh.go @@ -0,0 +1,50 @@ +package cache + +import ( + "zworld/engine/renderapi/buffer" + "zworld/engine/renderapi/command" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +type Mesh interface { + Draw(command.Buffer, int) + DrawInstanced(buf command.Buffer, startIndex, coount int) + Destroy() +} + +type vkMesh struct { + key string + elements int + idxType core1_0.IndexType + vertices buffer.T + indices buffer.T +} + +func (m *vkMesh) Draw(cmd command.Buffer, index int) { + m.DrawInstanced(cmd, index, 1) +} + +func (m *vkMesh) DrawInstanced(cmd command.Buffer, startIndex, count int) { + if m.elements <= 0 { + // nothing to draw + return + } + + cmd.CmdBindVertexBuffer(m.vertices, 0) + cmd.CmdBindIndexBuffers(m.indices, 0, m.idxType) + + // index of the object properties in the ssbo + cmd.CmdDrawIndexed(m.elements, count, 0, 0, startIndex) +} + +func (m *vkMesh) Destroy() { + if m.vertices != nil { + m.vertices.Destroy() + m.vertices = nil + } + if m.indices != nil { + m.indices.Destroy() + m.indices = nil + } +} diff --git a/engine/renderapi/cache/mesh_cache.go b/engine/renderapi/cache/mesh_cache.go new file mode 100644 index 0000000..cfdb72e --- /dev/null +++ b/engine/renderapi/cache/mesh_cache.go @@ -0,0 +1,91 @@ +package cache + +import ( + "zworld/engine/renderapi/buffer" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/vertex" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +type MeshCache T[vertex.Mesh, Mesh] + +type meshes struct { + device device.T + worker command.Worker +} + +func NewMeshCache(device device.T, worker command.Worker) MeshCache { + return New[vertex.Mesh, Mesh](&meshes{ + device: device, + worker: worker, + }) +} + +func (m *meshes) Instantiate(mesh vertex.Mesh, callback func(Mesh)) { + var cached *vkMesh + var vtxStage, idxStage buffer.T + + var idxType core1_0.IndexType + switch mesh.IndexSize() { + case 2: + idxType = core1_0.IndexTypeUInt16 + case 4: + idxType = core1_0.IndexTypeUInt32 + default: + panic("illegal index type") + } + + cached = &vkMesh{ + key: mesh.Key(), + elements: mesh.IndexCount(), + idxType: idxType, + } + if cached.elements == 0 { + // special case for empty meshes + callback(cached) + return + } + + m.worker.Queue(func(cmd command.Buffer) { + vtxSize := mesh.VertexSize() * mesh.VertexCount() + vtxStage = buffer.NewShared(m.device, "staging:vertex", vtxSize) + + idxSize := mesh.IndexSize() * mesh.IndexCount() + idxStage = buffer.NewShared(m.device, "staging:index", idxSize) + + vtxStage.Write(0, mesh.VertexData()) + vtxStage.Flush() + idxStage.Write(0, mesh.IndexData()) + idxStage.Flush() + + // allocate buffers + cached.vertices = buffer.NewRemote(m.device, mesh.Key()+":vertex", vtxSize, core1_0.BufferUsageVertexBuffer) + cached.indices = buffer.NewRemote(m.device, mesh.Key()+":index", idxSize, core1_0.BufferUsageIndexBuffer) + + cmd.CmdCopyBuffer(vtxStage, cached.vertices, core1_0.BufferCopy{ + Size: vtxSize, + }) + cmd.CmdCopyBuffer(idxStage, cached.indices, core1_0.BufferCopy{ + Size: idxSize, + }) + }) + m.worker.Submit(command.SubmitInfo{ + Marker: "MeshCache", + Callback: func() { + vtxStage.Destroy() + idxStage.Destroy() + callback(cached) + }, + }) +} + +func (m *meshes) Delete(mesh Mesh) { + mesh.Destroy() +} + +func (m *meshes) Destroy() {} + +func (m *meshes) Name() string { return "MeshCache" } +func (m *meshes) String() string { return "MeshCache" } diff --git a/engine/renderapi/cache/sampler_cache.go b/engine/renderapi/cache/sampler_cache.go new file mode 100644 index 0000000..efc84e9 --- /dev/null +++ b/engine/renderapi/cache/sampler_cache.go @@ -0,0 +1,158 @@ +package cache + +import ( + "zworld/engine/renderapi/color" + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/texture" +) + +type samplers struct { + textures TextureCache + desc *descriptor.SamplerArray + reverse map[string]*SamplerHandle + free map[int]bool + descriptors []texture.T + next int + + // the max age must be shorter than the max life of the texture cache. + // if using a per-frame sampler cache, then the max life time should be + // at most (texture max life) / (number of swapchain frames) + maxAge int + + // blank keeps a reference to a blank (white) texture + blank texture.T +} + +type SamplerHandle struct { + ID int + Texture texture.T + age int +} + +type SamplerCache interface { + T[texture.Ref, *SamplerHandle] + + // Assign a handle to a texture directly + Assign(texture.T) *SamplerHandle + + // Writes descriptor updates to the backing Sampler Array. + Flush() +} + +func NewSamplerCache(textures TextureCache, desc *descriptor.SamplerArray) SamplerCache { + samplers := &samplers{ + textures: textures, + desc: desc, + reverse: make(map[string]*SamplerHandle, 1000), + free: make(map[int]bool, 100), + descriptors: make([]texture.T, desc.Count), + next: 0, + maxAge: textures.MaxAge() / 4, + blank: textures.Fetch(color.White), + } + + // ensure id 0 is always blank + samplers.assignHandle(color.White) + + return samplers +} + +func (s *samplers) MaxAge() int { + return s.maxAge +} + +func (s *samplers) nextID() int { + // check free list + for handle := range s.free { + delete(s.free, handle) + return handle + } + + // allocate new handle + id := s.next + if id >= s.desc.Count { + panic("out of handles") + } + s.next++ + return id +} + +type Keyed interface { + Key() string +} + +func (s *samplers) assignHandle(ref Keyed) *SamplerHandle { + if handle, exists := s.reverse[ref.Key()]; exists { + // reset the age of the existing handle, if we have one + handle.age = 0 + return handle + } + handle := &SamplerHandle{ + ID: s.nextID(), + age: 0, + } + s.reverse[ref.Key()] = handle + return handle +} + +func (s *samplers) TryFetch(ref texture.Ref) (*SamplerHandle, bool) { + handle := s.assignHandle(ref) + var exists bool + if handle.Texture, exists = s.textures.TryFetch(ref); exists { + return handle, true + } + return nil, false +} + +func (s *samplers) Fetch(ref texture.Ref) *SamplerHandle { + handle := s.assignHandle(ref) + handle.Texture = s.textures.Fetch(ref) + return handle +} + +func (s *samplers) Assign(tex texture.T) *SamplerHandle { + handle := s.assignHandle(tex) + handle.Texture = tex + return handle +} + +func (s *samplers) Flush() { + s.Tick() + + for _, handle := range s.reverse { + tex := handle.Texture + if tex == nil { + continue + } + + if s.descriptors[handle.ID] == tex { + // texture hasnt changed, nothing to do. + continue + } + + // texture has changed! update descriptor + s.descriptors[handle.ID] = tex + s.desc.Set(handle.ID, tex) + } +} + +func (s *samplers) Tick() { + for ref, handle := range s.reverse { + // increase the age of the handle and check for eviction + handle.age++ + if handle.age > s.maxAge { + delete(s.reverse, ref) + s.free[handle.ID] = true + + // overwrite descriptor with blank texture + handle.Texture = s.blank + s.descriptors[handle.ID] = nil + s.desc.Set(handle.ID, s.blank) + } + } +} + +func (s *samplers) Destroy() { + // todo: unclear if theres anything to do here + // the backing texture cache holds all resources and will release them if unused +} diff --git a/engine/renderapi/cache/shader_cache.go b/engine/renderapi/cache/shader_cache.go new file mode 100644 index 0000000..dc31dc9 --- /dev/null +++ b/engine/renderapi/cache/shader_cache.go @@ -0,0 +1,37 @@ +package cache + +import ( + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/shader" +) + +type ShaderCache T[shader.Ref, shader.T] + +func NewShaderCache(dev device.T) ShaderCache { + return New[shader.Ref, shader.T](&shaders{ + device: dev, + }) +} + +type shaders struct { + device device.T +} + +func (s *shaders) Name() string { + return "Shaders" +} + +func (s *shaders) Instantiate(key shader.Ref, callback func(shader.T)) { + // load shader in a background goroutine + go func() { + shader := key.Load(s.device) + callback(shader) + }() +} + +func (s *shaders) Delete(shader shader.T) { + shader.Destroy() +} + +func (s *shaders) Destroy() { +} diff --git a/engine/renderapi/cache/texture_cache.go b/engine/renderapi/cache/texture_cache.go new file mode 100644 index 0000000..4b349ae --- /dev/null +++ b/engine/renderapi/cache/texture_cache.go @@ -0,0 +1,86 @@ +package cache + +import ( + "zworld/engine/renderapi/buffer" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/texture" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +type TextureCache T[texture.Ref, texture.T] + +func NewTextureCache(device device.T, worker command.Worker) TextureCache { + return New[texture.Ref, texture.T](&textures{ + device: device, + worker: worker, + }) +} + +type textures struct { + device device.T + worker command.Worker +} + +func (t *textures) Instantiate(ref texture.Ref, callback func(texture.T)) { + var stage buffer.T + var tex texture.T + + // transfer data to texture buffer + t.worker.Queue(func(cmd command.Buffer) { + // load image data + img := ref.ImageData() + + // args & defaults + args := ref.TextureArgs() + + // allocate texture + var err error + tex, err = texture.New(t.device, ref.Key(), img.Width, img.Height, img.Format, args) + if err != nil { + panic(err) + } + + // allocate staging buffer + stage = buffer.NewShared(t.device, "staging:texture", len(img.Buffer)) + + // write to staging buffer + stage.Write(0, img.Buffer) + stage.Flush() + + cmd.CmdImageBarrier( + core1_0.PipelineStageTopOfPipe, + core1_0.PipelineStageTransfer, + tex.Image(), + core1_0.ImageLayoutUndefined, + core1_0.ImageLayoutTransferDstOptimal, + core1_0.ImageAspectColor) + cmd.CmdCopyBufferToImage(stage, tex.Image(), core1_0.ImageLayoutTransferDstOptimal) + cmd.CmdImageBarrier( + core1_0.PipelineStageTransfer, + core1_0.PipelineStageFragmentShader, + tex.Image(), + core1_0.ImageLayoutTransferDstOptimal, + core1_0.ImageLayoutShaderReadOnlyOptimal, + core1_0.ImageAspectColor) + }) + t.worker.Submit(command.SubmitInfo{ + Marker: "TextureUpload", + Callback: func() { + stage.Destroy() + callback(tex) + }, + }) +} + +func (t *textures) Delete(tex texture.T) { + tex.Destroy() +} + +func (t *textures) Destroy() { + +} + +func (t *textures) Name() string { return "TextureCache" } +func (t *textures) String() string { return "TextureCache" } diff --git a/engine/renderapi/color/color.go b/engine/renderapi/color/color.go new file mode 100644 index 0000000..0c44a03 --- /dev/null +++ b/engine/renderapi/color/color.go @@ -0,0 +1,181 @@ +package color + +import ( + "fmt" + "image/color" + + "zworld/engine/renderapi/image" + "zworld/engine/renderapi/texture" + "zworld/plugins/math/byte4" + "zworld/plugins/math/vec3" + "zworld/plugins/math/vec4" +) + +// Predefined Colors +var ( + White = T{1, 1, 1, 1} + Black = T{0, 0, 0, 1} + Red = T{1, 0, 0, 1} + Green = T{0, 1, 0, 1} + Blue = T{0, 0, 1, 1} + Purple = T{1, 0, 1, 1} + Yellow = T{1, 1, 0, 1} + Cyan = T{0, 1, 1, 1} + Transparent = T{0, 0, 0, 0} + None = T{0, 0, 0, 0} + + DarkGrey = T{0.2, 0.2, 0.2, 1} +) + +// T holds 32-bit RGBA colors +type T struct { + R, G, B, A float32 +} + +var _ texture.Ref = T{} + +// Color4 creates a color struct from its RGBA components +func RGBA(r, g, b, a float32) T { + return T{r, g, b, a} +} + +func RGB(r, g, b float32) T { + return T{r, g, b, 1} +} + +func RGBA8(r, g, b, a uint8) T { + return RGBA(float32(r)/255, float32(g)/255, float32(b)/255, float32(a)/255) +} + +func RGB8(r, g, b uint8) T { + return RGBA8(r, g, b, 255) +} + +// RGBA returns an 8-bit RGBA image/color +func (c T) RGBA() color.RGBA { + return color.RGBA{ + uint8(255.0 * c.R), + uint8(255.0 * c.G), + uint8(255.0 * c.B), + uint8(255.0 * c.A), + } +} + +func FromVec3(v vec3.T) T { + return RGB(v.X, v.Y, v.Z) +} + +// Vec3 returns a vec3 containing the RGB components of the color +func (c T) Vec3() vec3.T { + return vec3.New(c.R, c.G, c.B) +} + +func FromVec4(v vec4.T) T { + return RGBA(v.X, v.Y, v.Z, v.W) +} + +// Vec4 returns a vec4 containing the RGBA components of the color +func (c T) Vec4() vec4.T { + return vec4.New(c.R, c.G, c.B, c.A) +} + +func (c T) Byte4() byte4.T { + return byte4.New( + byte(255.0*c.R), + byte(255.0*c.G), + byte(255.0*c.B), + byte(255.0*c.A)) +} + +func (c T) String() string { + return fmt.Sprintf("(R:%.2f G:%.2f B:%.2f A:%.2f)", c.R, c.G, c.B, c.A) +} + +var hexDigits = []byte("0123456789abcdef") + +func (c T) Hex() string { + rgba := c.Byte4() + bytes := make([]byte, 9) + bytes[0] = '#' + bytes[1] = hexDigits[rgba.X>>4] + bytes[2] = hexDigits[rgba.X&0x0F] + bytes[3] = hexDigits[rgba.Y>>4] + bytes[4] = hexDigits[rgba.Y&0x0F] + bytes[5] = hexDigits[rgba.Z>>4] + bytes[6] = hexDigits[rgba.Z&0x0F] + if c.A < 1 { + bytes[7] = hexDigits[rgba.W>>4] + bytes[8] = hexDigits[rgba.W&0x0F] + } else { + bytes = bytes[:7] + } + return string(bytes) +} + +// WithAlpha returns a new color with a modified alpha value +func (c T) WithAlpha(a float32) T { + c.A = a + return c +} + +func Hex(s string) T { + if s[0] != '#' { + panic("invalid color value") + } + + hexToByte := func(b byte) byte { + switch { + case b >= '0' && b <= '9': + return b - '0' + case b >= 'a' && b <= 'f': + return b - 'a' + 10 + case b >= 'A' && b <= 'F': + return b - 'A' + 10 + } + panic("invalid color value") + } + + c := T{A: 1} + switch len(s) { + case 9: + c.A = float32(hexToByte(s[7])<<4+hexToByte(s[6])) / 255 + fallthrough + case 7: + c.R = float32(hexToByte(s[1])<<4+hexToByte(s[2])) / 255 + c.G = float32(hexToByte(s[3])<<4+hexToByte(s[4])) / 255 + c.B = float32(hexToByte(s[5])<<4+hexToByte(s[6])) / 255 + case 4: + c.R = float32(hexToByte(s[1])*17) / 255 + c.G = float32(hexToByte(s[2])*17) / 255 + c.B = float32(hexToByte(s[3])*17) / 255 + default: + panic("invalid color value") + } + return c +} + +// +// implement texture reference interface, so that colors may be easily loaded as textures +// + +func (c T) Key() string { return c.Hex() } +func (c T) Version() int { return 1 } + +func (c T) ImageData() *image.Data { + rgba := c.Byte4() + return &image.Data{ + Width: 1, + Height: 1, + Format: image.FormatRGBA8Unorm, + Buffer: []byte{ + rgba.X, rgba.Y, rgba.Z, rgba.W, + }, + } +} + +func (c T) TextureArgs() texture.Args { + return texture.Args{ + Filter: texture.FilterNearest, + Wrap: texture.WrapClamp, + } +} diff --git a/engine/renderapi/color/color_suite_test.go b/engine/renderapi/color/color_suite_test.go new file mode 100644 index 0000000..7291eb8 --- /dev/null +++ b/engine/renderapi/color/color_suite_test.go @@ -0,0 +1,41 @@ +package color_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "zworld/engine/renderapi/color" +) + +func TestColor(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "renderapi/color") +} + +var _ = Describe("colors", func() { + It("converts from hex codes", func() { + c := color.Hex("#123456") + Expect(c.R).To(BeNumerically("~", float32(0x12)/255.0)) + Expect(c.G).To(BeNumerically("~", float32(0x34)/255.0)) + Expect(c.B).To(BeNumerically("~", float32(0x56)/255.0)) + + a := color.Hex("#000000f0") + Expect(a.A).To(BeNumerically("~", float32(0xF0)/255.0)) + }) + + It("converts to hex codes", func() { + c := color.RGB( + float32(0x12)/255.0, + float32(0x34)/255.0, + float32(0x56)/255.0, + ) + Expect(c.Hex()).To(Equal("#123456")) + + a := color.RGBA( + 0, 0, 0, + float32(0xF0)/255.0, + ) + Expect(a.Hex()).To(Equal("#000000f0")) + }) +}) diff --git a/engine/renderapi/color/palette.go b/engine/renderapi/color/palette.go new file mode 100644 index 0000000..579875b --- /dev/null +++ b/engine/renderapi/color/palette.go @@ -0,0 +1,28 @@ +package color + +// A Palette is a list of colors +type Palette []T + +// RawPalette creates a palette from a list of hex integers +func RawPalette(colors ...int) Palette { + palette := make(Palette, len(colors)) + for i, clr := range colors { + palette[i] = RGBA( + float32((clr>>16)&0xFF)/255.0, + float32((clr>>8)&0xFF)/255.0, + float32((clr>>0)&0xFF)/255.0, + 1.0) + } + return palette +} + +// DefaultPalette https://lospec.com/palette-list/broken-facility +var DefaultPalette = RawPalette( + 0x24211e, 0x898377, 0xada99e, 0xcccac4, 0xf9f8f7, + 0x563735, 0x835748, 0xa37254, 0xb59669, 0xcab880, + 0x4d1c2d, 0x98191e, 0xd12424, 0xdd4b63, 0xf379e2, + 0xc86826, 0xd8993f, 0xe8c04f, 0xf2db89, 0xf8f1c6, + 0x17601f, 0x488c36, 0x7abd40, 0xa4cf41, 0xcdde5e, + 0x5044ba, 0x5e9ccc, 0x7fc6ce, 0x9de2df, 0xcaf1ea, + 0x202c56, 0x3f2d6d, 0x772673, 0xb9284f, 0xcb5135, + 0xeda7d8, 0xf3bedd, 0xdbebeb, 0xe9dde8, 0xd5c4df) diff --git a/engine/renderapi/command/buffer.go b/engine/renderapi/command/buffer.go new file mode 100644 index 0000000..7a81a25 --- /dev/null +++ b/engine/renderapi/command/buffer.go @@ -0,0 +1,329 @@ +package command + +import ( + "reflect" + "unsafe" + + "zworld/engine/renderapi/buffer" + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/framebuffer" + "zworld/engine/renderapi/image" + "zworld/engine/renderapi/pipeline" + "zworld/engine/renderapi/renderpass" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +type Buffer interface { + device.Resource[core1_0.CommandBuffer] + + Reset() + Begin() + End() + + CmdCopyBuffer(src, dst buffer.T, regions ...core1_0.BufferCopy) + CmdBindGraphicsPipeline(pipe pipeline.T) + CmdBindGraphicsDescriptor(sets descriptor.Set) + CmdBindVertexBuffer(vtx buffer.T, offset int) + CmdBindIndexBuffers(idx buffer.T, offset int, kind core1_0.IndexType) + CmdDraw(vertexCount, instanceCount, firstVertex, firstInstance int) + CmdDrawIndexed(indexCount, instanceCount, firstIndex, vertexOffset, firstInstance int) + CmdBeginRenderPass(pass renderpass.T, framebuffer framebuffer.T) + CmdNextSubpass() + CmdEndRenderPass() + CmdSetViewport(x, y, w, h int) core1_0.Viewport + CmdSetScissor(x, y, w, h int) core1_0.Rect2D + CmdPushConstant(stages core1_0.ShaderStageFlags, offset int, value any) + CmdImageBarrier(srcMask, dstMask core1_0.PipelineStageFlags, image image.T, oldLayout, newLayout core1_0.ImageLayout, aspects core1_0.ImageAspectFlags) + CmdCopyBufferToImage(source buffer.T, dst image.T, layout core1_0.ImageLayout) + CmdCopyImageToBuffer(src image.T, srcLayout core1_0.ImageLayout, aspect core1_0.ImageAspectFlags, dst buffer.T) + CmdConvertImage(src image.T, srcLayout core1_0.ImageLayout, dst image.T, dstLayout core1_0.ImageLayout, aspects core1_0.ImageAspectFlags) + CmdCopyImage(src image.T, srcLayout core1_0.ImageLayout, dst image.T, dstLayout core1_0.ImageLayout, aspects core1_0.ImageAspectFlags) +} + +type buf struct { + ptr core1_0.CommandBuffer + pool core1_0.CommandPool + device device.T + + // cached bindings + pipeline pipeline.T + vertex bufferBinding + index bufferBinding + scissor core1_0.Rect2D + viewport core1_0.Viewport +} + +type bufferBinding struct { + buffer core1_0.Buffer + offset int + indexType core1_0.IndexType +} + +func newBuffer(device device.T, pool core1_0.CommandPool, ptr core1_0.CommandBuffer) Buffer { + return &buf{ + ptr: ptr, + pool: pool, + device: device, + } +} + +func (b *buf) Ptr() core1_0.CommandBuffer { + return b.ptr +} + +func (b *buf) Destroy() { + b.ptr.Free() + b.ptr = nil +} + +func (b *buf) Reset() { + b.ptr.Reset(core1_0.CommandBufferResetReleaseResources) +} + +func (b *buf) Begin() { + if _, err := b.ptr.Begin(core1_0.CommandBufferBeginInfo{}); err != nil { + panic(err) + } +} + +func (b *buf) End() { + b.ptr.End() +} + +func (b *buf) CmdCopyBuffer(src, dst buffer.T, regions ...core1_0.BufferCopy) { + if len(regions) == 0 { + regions = []core1_0.BufferCopy{ + { + SrcOffset: 0, + DstOffset: 0, + Size: src.Size(), + }, + } + } + if src.Ptr() == nil || dst.Ptr() == nil { + panic("copy to/from null buffer") + } + b.ptr.CmdCopyBuffer(src.Ptr(), dst.Ptr(), regions) +} + +func (b *buf) CmdBindGraphicsPipeline(pipe pipeline.T) { + // if b.pipeline != nil && b.pipeline.Ptr() == pipe.Ptr() { + // return + // } + b.ptr.CmdBindPipeline(core1_0.PipelineBindPointGraphics, pipe.Ptr()) + b.pipeline = pipe +} + +func (b *buf) CmdBindGraphicsDescriptor(set descriptor.Set) { + if b.pipeline == nil { + panic("bind graphics pipeline first") + } + b.ptr.CmdBindDescriptorSets(core1_0.PipelineBindPointGraphics, b.pipeline.Layout().Ptr(), 0, []core1_0.DescriptorSet{set.Ptr()}, nil) +} + +func (b *buf) CmdBindVertexBuffer(vtx buffer.T, offset int) { + binding := bufferBinding{buffer: vtx.Ptr(), offset: offset} + if b.vertex == binding { + return + } + b.ptr.CmdBindVertexBuffers(0, []core1_0.Buffer{vtx.Ptr()}, []int{offset}) + b.vertex = binding +} + +func (b *buf) CmdBindIndexBuffers(idx buffer.T, offset int, kind core1_0.IndexType) { + binding := bufferBinding{buffer: idx.Ptr(), offset: offset, indexType: kind} + if b.index == binding { + return + } + b.ptr.CmdBindIndexBuffer(idx.Ptr(), offset, kind) + b.index = binding +} + +func (b *buf) CmdDraw(vertexCount, instanceCount, firstVertex, firstInstance int) { + b.ptr.CmdDraw(vertexCount, instanceCount, uint32(firstVertex), uint32(firstInstance)) +} + +func (b *buf) CmdDrawIndexed(indexCount, instanceCount, firstIndex, vertexOffset, firstInstance int) { + b.ptr.CmdDrawIndexed(indexCount, instanceCount, uint32(firstIndex), vertexOffset, uint32(firstInstance)) +} + +func (b *buf) CmdBeginRenderPass(pass renderpass.T, framebuffer framebuffer.T) { + clear := pass.Clear() + w, h := framebuffer.Size() + + b.ptr.CmdBeginRenderPass(core1_0.SubpassContentsInline, core1_0.RenderPassBeginInfo{ + RenderPass: pass.Ptr(), + Framebuffer: framebuffer.Ptr(), + RenderArea: core1_0.Rect2D{ + Offset: core1_0.Offset2D{}, + Extent: core1_0.Extent2D{ + Width: w, + Height: h, + }, + }, + ClearValues: clear, + }) + + b.CmdSetViewport(0, 0, w, h) + b.CmdSetScissor(0, 0, w, h) +} + +func (b *buf) CmdEndRenderPass() { + b.ptr.CmdEndRenderPass() +} + +func (b *buf) CmdNextSubpass() { + b.ptr.CmdNextSubpass(core1_0.SubpassContentsInline) +} + +func (b *buf) CmdSetViewport(x, y, w, h int) core1_0.Viewport { + prev := b.viewport + b.viewport = core1_0.Viewport{ + X: float32(x), + Y: float32(y), + Width: float32(w), + Height: float32(h), + MinDepth: 0, + MaxDepth: 1, + } + b.ptr.CmdSetViewport([]core1_0.Viewport{ + b.viewport, + }) + return prev +} + +func (b *buf) CmdSetScissor(x, y, w, h int) core1_0.Rect2D { + prev := b.scissor + b.scissor = core1_0.Rect2D{ + Offset: core1_0.Offset2D{ + X: x, + Y: y, + }, + Extent: core1_0.Extent2D{ + Width: w, + Height: h, + }, + } + b.ptr.CmdSetScissor([]core1_0.Rect2D{ + b.scissor, + }) + return prev +} + +func (b *buf) CmdPushConstant(stages core1_0.ShaderStageFlags, offset int, value any) { + if b.pipeline == nil { + panic("bind graphics pipeline first") + } + // this is awkward + // ptr := reflect.ValueOf(value).UnsafePointer() + size := reflect.ValueOf(value).Elem().Type().Size() + ptr := reflect.ValueOf(value).UnsafePointer() + valueBytes := make([]byte, size) + + device.Memcpy(unsafe.Pointer(&valueBytes[0]), ptr, int(size)) + b.ptr.CmdPushConstants(b.pipeline.Layout().Ptr(), stages, offset, valueBytes) +} + +func (b *buf) CmdImageBarrier(srcMask, dstMask core1_0.PipelineStageFlags, image image.T, oldLayout, newLayout core1_0.ImageLayout, aspects core1_0.ImageAspectFlags) { + b.ptr.CmdPipelineBarrier(core1_0.PipelineStageFlags(srcMask), core1_0.PipelineStageFlags(dstMask), core1_0.DependencyFlags(0), nil, nil, []core1_0.ImageMemoryBarrier{ + { + OldLayout: oldLayout, + NewLayout: newLayout, + Image: image.Ptr(), + SubresourceRange: core1_0.ImageSubresourceRange{ + AspectMask: core1_0.ImageAspectFlags(aspects), + LayerCount: 1, + LevelCount: 1, + }, + SrcAccessMask: core1_0.AccessMemoryRead | core1_0.AccessMemoryWrite, + DstAccessMask: core1_0.AccessMemoryRead | core1_0.AccessMemoryWrite, + }, + }) +} + +func (b *buf) CmdCopyBufferToImage(source buffer.T, dst image.T, layout core1_0.ImageLayout) { + b.ptr.CmdCopyBufferToImage(source.Ptr(), dst.Ptr(), layout, []core1_0.BufferImageCopy{ + { + ImageSubresource: core1_0.ImageSubresourceLayers{ + AspectMask: core1_0.ImageAspectColor, + LayerCount: 1, + }, + ImageExtent: core1_0.Extent3D{ + Width: dst.Width(), + Height: dst.Height(), + Depth: 1, + }, + }, + }) +} + +func (b *buf) CmdCopyImageToBuffer(src image.T, srcLayout core1_0.ImageLayout, aspects core1_0.ImageAspectFlags, dst buffer.T) { + b.ptr.CmdCopyImageToBuffer(src.Ptr(), srcLayout, dst.Ptr(), []core1_0.BufferImageCopy{ + { + ImageSubresource: core1_0.ImageSubresourceLayers{ + AspectMask: core1_0.ImageAspectFlags(aspects), + LayerCount: 1, + }, + ImageExtent: core1_0.Extent3D{ + Width: src.Width(), + Height: src.Height(), + Depth: 1, + }, + }, + }) +} + +func (b *buf) CmdConvertImage(src image.T, srcLayout core1_0.ImageLayout, dst image.T, dstLayout core1_0.ImageLayout, aspects core1_0.ImageAspectFlags) { + b.ptr.CmdBlitImage(src.Ptr(), srcLayout, dst.Ptr(), dstLayout, []core1_0.ImageBlit{ + { + SrcOffsets: [2]core1_0.Offset3D{ + {X: 0, Y: 0, Z: 0}, + {X: src.Width(), Y: src.Height(), Z: 1}, + }, + SrcSubresource: core1_0.ImageSubresourceLayers{ + AspectMask: core1_0.ImageAspectFlags(aspects), + MipLevel: 0, + BaseArrayLayer: 0, + LayerCount: 1, + }, + DstOffsets: [2]core1_0.Offset3D{ + {X: 0, Y: 0, Z: 0}, + {X: dst.Width(), Y: dst.Height(), Z: 1}, + }, + DstSubresource: core1_0.ImageSubresourceLayers{ + AspectMask: core1_0.ImageAspectFlags(aspects), + MipLevel: 0, + BaseArrayLayer: 0, + LayerCount: 1, + }, + }, + }, core1_0.FilterNearest) +} + +func (b *buf) CmdCopyImage(src image.T, srcLayout core1_0.ImageLayout, dst image.T, dstLayout core1_0.ImageLayout, aspects core1_0.ImageAspectFlags) { + b.ptr.CmdCopyImage(src.Ptr(), srcLayout, dst.Ptr(), dstLayout, []core1_0.ImageCopy{ + { + SrcOffset: core1_0.Offset3D{X: 0, Y: 0, Z: 0}, + SrcSubresource: core1_0.ImageSubresourceLayers{ + AspectMask: core1_0.ImageAspectFlags(aspects), + MipLevel: 0, + BaseArrayLayer: 0, + LayerCount: 1, + }, + DstOffset: core1_0.Offset3D{X: 0, Y: 0, Z: 0}, + DstSubresource: core1_0.ImageSubresourceLayers{ + AspectMask: core1_0.ImageAspectFlags(aspects), + MipLevel: 0, + BaseArrayLayer: 0, + LayerCount: 1, + }, + Extent: core1_0.Extent3D{ + Width: dst.Width(), + Height: dst.Height(), + Depth: 1, + }, + }, + }) +} diff --git a/engine/renderapi/command/pool.go b/engine/renderapi/command/pool.go new file mode 100644 index 0000000..b348720 --- /dev/null +++ b/engine/renderapi/command/pool.go @@ -0,0 +1,62 @@ +package command + +import ( + "github.com/vkngwrapper/core/v2/core1_0" + "zworld/engine/renderapi/device" + "zworld/engine/util" +) + +type Pool interface { + device.Resource[core1_0.CommandPool] + + Allocate(level core1_0.CommandBufferLevel) Buffer + AllocateBuffers(level core1_0.CommandBufferLevel, count int) []Buffer +} + +type pool struct { + ptr core1_0.CommandPool + device device.T +} + +func NewPool(device device.T, flags core1_0.CommandPoolCreateFlags, queueFamilyIdx int) Pool { + ptr, _, err := device.Ptr().CreateCommandPool(nil, core1_0.CommandPoolCreateInfo{ + Flags: flags, + QueueFamilyIndex: queueFamilyIdx, + }) + if err != nil { + panic(err) + } + return &pool{ + ptr: ptr, + device: device, + } +} + +func (p *pool) Ptr() core1_0.CommandPool { + return p.ptr +} + +func (p *pool) Destroy() { + p.ptr.Destroy(nil) + p.ptr = nil +} + +func (p *pool) Allocate(level core1_0.CommandBufferLevel) Buffer { + buffers := p.AllocateBuffers(level, 1) + return buffers[0] +} + +func (p *pool) AllocateBuffers(level core1_0.CommandBufferLevel, count int) []Buffer { + ptrs, _, err := p.device.Ptr().AllocateCommandBuffers(core1_0.CommandBufferAllocateInfo{ + CommandPool: p.ptr, + Level: level, + CommandBufferCount: count, + }) + if err != nil { + panic(err) + } + + return util.Map(ptrs, func(ptr core1_0.CommandBuffer) Buffer { + return newBuffer(p.device, p.ptr, ptr) + }) +} diff --git a/engine/renderapi/command/recorder.go b/engine/renderapi/command/recorder.go new file mode 100644 index 0000000..6061bf5 --- /dev/null +++ b/engine/renderapi/command/recorder.go @@ -0,0 +1,26 @@ +package command + +type Recorder interface { + Record(CommandFn) + Apply(Buffer) +} + +type recorder struct { + parts []CommandFn +} + +func NewRecorder() Recorder { + return &recorder{ + parts: make([]CommandFn, 0, 64), + } +} + +func (r recorder) Apply(cmd Buffer) { + for _, part := range r.parts { + part(cmd) + } +} + +func (r *recorder) Record(cmd CommandFn) { + r.parts = append(r.parts, cmd) +} diff --git a/engine/renderapi/command/thread_worker.go b/engine/renderapi/command/thread_worker.go new file mode 100644 index 0000000..3feee5f --- /dev/null +++ b/engine/renderapi/command/thread_worker.go @@ -0,0 +1,70 @@ +package command + +import ( + "runtime" +) + +type ThreadWorker struct { + name string + buffer int + work chan func() +} + +func NewThreadWorker(name string, buffer int, locked bool) *ThreadWorker { + w := &ThreadWorker{ + name: name, + buffer: buffer, + work: make(chan func(), buffer), + } + go w.workloop(locked) + return w +} + +func (tw *ThreadWorker) workloop(locked bool) { + // lock the worker to its current thread + if locked { + runtime.LockOSThread() + } + + // work loop + for { + work, more := <-tw.work + if !more { + break + } + work() + } +} + +// Invoke schedules a callback to be called from the worker thread +func (tw *ThreadWorker) Invoke(callback func()) { + tw.work <- callback +} + +// InvokeSync schedules a callback to be called on the worker thread, +// and blocks until the callback is finished. +func (tw *ThreadWorker) InvokeSync(callback func()) { + done := make(chan struct{}) + tw.work <- func() { + callback() + done <- struct{}{} + } + <-done +} + +// Aborts the worker, cancelling any pending work. +func (tw *ThreadWorker) Abort() { + close(tw.work) +} + +// Stop the worker and release any resources. Blocks until all work in completed. +func (tw *ThreadWorker) Stop() { + tw.InvokeSync(func() { + close(tw.work) + }) +} + +// Flush blocks the caller until all pending work is completed +func (tw *ThreadWorker) Flush() { + tw.InvokeSync(func() {}) +} diff --git a/engine/renderapi/command/worker.go b/engine/renderapi/command/worker.go new file mode 100644 index 0000000..496dbcd --- /dev/null +++ b/engine/renderapi/command/worker.go @@ -0,0 +1,153 @@ +package command + +import ( + "fmt" + "runtime/debug" + + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/sync" + "zworld/engine/util" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/core/v2/driver" +) + +type CommandFn func(Buffer) + +// Workers manage a command pool thread +type Worker interface { + Ptr() core1_0.Queue + Queue(CommandFn) + Submit(SubmitInfo) + Destroy() + Flush() + Invoke(func()) +} + +type Workers []Worker + +type worker struct { + device device.T + name string + queue core1_0.Queue + pool Pool + batch []Buffer + work *ThreadWorker +} + +func NewWorker(device device.T, name string, queueFlags core1_0.QueueFlags, queueIndex int) Worker { + pool := NewPool(device, core1_0.CommandPoolCreateTransient, queueIndex) + queue := device.GetQueue(queueIndex, queueFlags) + + name = fmt.Sprintf("%s:%d:%x", name, queueIndex, queue.Handle()) + device.SetDebugObjectName(driver.VulkanHandle(queue.Handle()), core1_0.ObjectTypeQueue, name) + + return &worker{ + device: device, + name: name, + queue: queue, + pool: pool, + batch: make([]Buffer, 0, 128), + work: NewThreadWorker(name, 100, true), + } +} + +func (w *worker) Ptr() core1_0.Queue { + return w.queue +} + +// Invoke schedules a callback to be called from the worker thread +func (w *worker) Invoke(callback func()) { + w.work.Invoke(callback) +} + +func (w *worker) Queue(batch CommandFn) { + w.work.Invoke(func() { + w.enqueue(batch) + }) +} + +func (w *worker) enqueue(batch CommandFn) { + // todo: dont make a command buffer for each call to Queue() !! + // instead, allocate and record everything that we've batched just prior to submission + + // allocate a new buffer + buf := w.pool.Allocate(core1_0.CommandBufferLevelPrimary) + + // record commands + buf.Begin() + defer buf.End() + batch(buf) + + // append to the next batch + w.batch = append(w.batch, buf) +} + +type SubmitInfo struct { + Marker string + Wait []Wait + Signal []sync.Semaphore + Callback func() +} + +type Wait struct { + Semaphore sync.Semaphore + Mask core1_0.PipelineStageFlags +} + +// Submit the current batch of command buffers +// Blocks until the queue submission is confirmed +func (w *worker) Submit(submit SubmitInfo) { + w.work.Invoke(func() { + w.submit(submit) + }) +} + +func (w *worker) submit(submit SubmitInfo) { + debug.SetPanicOnFault(true) + buffers := util.Map(w.batch, func(buf Buffer) core1_0.CommandBuffer { return buf.Ptr() }) + + // create a cleanup callback + // todo: reuse fences + fence := sync.NewFence(w.device, submit.Marker, false) + + // submit buffers to the given queue + w.queue.Submit(fence.Ptr(), []core1_0.SubmitInfo{ + { + CommandBuffers: buffers, + SignalSemaphores: util.Map(submit.Signal, func(sem sync.Semaphore) core1_0.Semaphore { return sem.Ptr() }), + WaitSemaphores: util.Map(submit.Wait, func(w Wait) core1_0.Semaphore { return w.Semaphore.Ptr() }), + WaitDstStageMask: util.Map(submit.Wait, func(w Wait) core1_0.PipelineStageFlags { return w.Mask }), + }, + }) + + // clear batch slice but keep memory + w.batch = w.batch[:0] + + // fire up a cleanup goroutine that will execute when the work fence is signalled + go func() { + fence.Wait() + fence.Destroy() + + w.work.Invoke(func() { + // free buffers + if len(buffers) > 0 { + w.device.Ptr().FreeCommandBuffers(buffers) + } + + // run callback (on the worker thead) + if submit.Callback != nil { + submit.Callback() + } + }) + }() +} + +func (w *worker) Destroy() { + w.work.Stop() + w.pool.Destroy() +} + +func (w *worker) Flush() { + w.work.Flush() +} diff --git a/engine/renderapi/descriptor/descriptor.go b/engine/renderapi/descriptor/descriptor.go new file mode 100644 index 0000000..a916d5e --- /dev/null +++ b/engine/renderapi/descriptor/descriptor.go @@ -0,0 +1,21 @@ +package descriptor + +import ( + "zworld/engine/renderapi/device" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/extensions/v2/ext_descriptor_indexing" +) + +type Descriptor interface { + Initialize(device.T) + LayoutBinding(int) core1_0.DescriptorSetLayoutBinding + BindingFlags() ext_descriptor_indexing.DescriptorBindingFlags + Bind(Set, int) + Destroy() +} + +type VariableDescriptor interface { + Descriptor + MaxCount() int +} diff --git a/engine/renderapi/descriptor/descriptor_struct.go b/engine/renderapi/descriptor/descriptor_struct.go new file mode 100644 index 0000000..1517f9c --- /dev/null +++ b/engine/renderapi/descriptor/descriptor_struct.go @@ -0,0 +1,115 @@ +package descriptor + +import ( + "errors" + "fmt" + "reflect" +) + +var ErrDescriptorType = errors.New("invalid descriptor struct") + +type Resolver interface { + Descriptor(string) (int, bool) +} + +func ParseDescriptorStruct[S Set](template S) (map[string]Descriptor, error) { + ptr := reflect.ValueOf(template) + if ptr.Kind() != reflect.Pointer { + return nil, fmt.Errorf("%w: template must be a pointer to struct", ErrDescriptorType) + } + + templateStruct := ptr.Elem() + structName := templateStruct.Type().Name() + if templateStruct.Kind() != reflect.Struct { + return nil, fmt.Errorf("%w: template %s must be a pointer to struct", ErrDescriptorType, structName) + } + + descriptors := make(map[string]Descriptor) + for i := 0; i < templateStruct.NumField(); i++ { + fieldName := templateStruct.Type().Field(i).Name + templateField := templateStruct.Field(i) + + if fieldName == "Set" { + // Field named Set must be an embedding of descriptor.Set + if !templateField.IsNil() { + return nil, fmt.Errorf("%w: %s member called Set must be nil", ErrDescriptorType, structName) + } + } else { + // template field must be a non-nil pointer + if templateField.Kind() != reflect.Pointer { + return nil, fmt.Errorf("%w: %s.%s is not a pointer, was %s", ErrDescriptorType, structName, fieldName, templateField.Kind()) + } + if templateField.IsNil() { + return nil, fmt.Errorf("%w: %s.%s is must not be nil", ErrDescriptorType, structName, fieldName) + } + + // ensure the value is a Descriptor interface + if !templateField.CanInterface() { + return nil, fmt.Errorf("%w: %s.%s is not an interface", ErrDescriptorType, structName, fieldName) + } + descriptor, isDescriptor := templateField.Interface().(Descriptor) + if !isDescriptor { + return nil, fmt.Errorf("%w: %s.%s is not a Descriptor", ErrDescriptorType, structName, fieldName) + } + + // ensure only the last descriptor element is of variable length + _, isVariableLength := descriptor.(VariableDescriptor) + if isVariableLength { + isLast := i == templateStruct.NumField()-1 + if !isLast { + return nil, fmt.Errorf("%w: %s.%s is variable length, but not the last element", ErrDescriptorType, structName, fieldName) + } + } + + descriptors[fieldName] = descriptor + } + } + + return descriptors, nil +} + +// CopyDescriptorStruct instantiates a descriptor struct according to the given template. +// Assumes that the template has passed validation beforehand. +func CopyDescriptorStruct[S Set](template S, blank Set, resolver Resolver) (S, []Descriptor) { + // dereference + ptr := reflect.ValueOf(template) + templateStruct := ptr.Elem() + + copyPtr := reflect.New(templateStruct.Type()) + + descriptors := make([]Descriptor, 0, templateStruct.NumField()) + for i := 0; i < templateStruct.NumField(); i++ { + copyField := copyPtr.Elem().Field(i) + fieldName := templateStruct.Type().Field(i).Name + + if fieldName == "Set" { + // store Set embedding reference + copyField.Set(reflect.ValueOf(blank)) + } else { + templateField := templateStruct.Field(i) + + // create a copy of the template field's value + fieldValue := templateField.Elem() + valueCopy := reflect.New(fieldValue.Type()) + valueCopy.Elem().Set(fieldValue) + + // write the value to the copied struct + copyField.Set(valueCopy) + + // cast the copied value to a Descriptor interface + descriptor := valueCopy.Interface().(Descriptor) + + // bind it to our blank descriptor set + binding, exists := resolver.Descriptor(fieldName) + if !exists { + panic(fmt.Errorf("unresolved descriptor: %s", fieldName)) + } + descriptor.Bind(blank, binding) + descriptors = append(descriptors, descriptor) + } + } + + // finally, cast to correct type + copy := copyPtr.Interface().(S) + return copy, descriptors +} diff --git a/engine/renderapi/descriptor/descriptor_struct_test.go b/engine/renderapi/descriptor/descriptor_struct_test.go new file mode 100644 index 0000000..b1f40cc --- /dev/null +++ b/engine/renderapi/descriptor/descriptor_struct_test.go @@ -0,0 +1,45 @@ +package descriptor_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "zworld/engine/renderapi/descriptor" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +var _ = Describe("descriptor struct parsing", func() { + type TestSet struct { + Set + Diffuse *Sampler + } + + It("correctly parses valid structs", func() { + set := TestSet{ + Diffuse: &Sampler{ + Stages: core1_0.StageAll, + }, + } + desc, err := ParseDescriptorStruct(&set) + Expect(err).ToNot(HaveOccurred()) + Expect(desc).To(HaveLen(1), "expected to find diffuse descriptor") + }) + + It("rejects unset descriptor fields", func() { + set := TestSet{ + Diffuse: nil, + } + _, err := ParseDescriptorStruct(&set) + Expect(err).To(HaveOccurred()) + }) + + It("rejects non-pointer fields", func() { + type FailSet struct { + Set + Diffuse Sampler + } + set := FailSet{} + _, err := ParseDescriptorStruct(&set) + Expect(err).To(HaveOccurred()) + }) +}) diff --git a/engine/renderapi/descriptor/descriptor_suite_test.go b/engine/renderapi/descriptor/descriptor_suite_test.go new file mode 100644 index 0000000..e095970 --- /dev/null +++ b/engine/renderapi/descriptor/descriptor_suite_test.go @@ -0,0 +1,13 @@ +package descriptor_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestDescriptor(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "renderapi/descriptor") +} diff --git a/engine/renderapi/descriptor/input_attachment.go b/engine/renderapi/descriptor/input_attachment.go new file mode 100644 index 0000000..f6d08c8 --- /dev/null +++ b/engine/renderapi/descriptor/input_attachment.go @@ -0,0 +1,71 @@ +package descriptor + +import ( + "fmt" + + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/image" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/extensions/v2/ext_descriptor_indexing" +) + +type InputAttachment struct { + Stages core1_0.ShaderStageFlags + Layout core1_0.ImageLayout + + binding int + view core1_0.ImageView + set Set +} + +var _ Descriptor = &InputAttachment{} + +func (d *InputAttachment) Initialize(device device.T) { + if d.Layout == 0 { + d.Layout = core1_0.ImageLayoutShaderReadOnlyOptimal + } +} + +func (d *InputAttachment) String() string { + return fmt.Sprintf("Input:%d", d.binding) +} + +func (d *InputAttachment) Destroy() {} + +func (d *InputAttachment) Bind(set Set, binding int) { + d.set = set + d.binding = binding +} + +func (d *InputAttachment) Set(view image.View) { + d.view = view.Ptr() + d.write() +} + +func (d *InputAttachment) LayoutBinding(binding int) core1_0.DescriptorSetLayoutBinding { + d.binding = binding + return core1_0.DescriptorSetLayoutBinding{ + Binding: binding, + DescriptorType: core1_0.DescriptorTypeInputAttachment, + DescriptorCount: 1, + StageFlags: core1_0.ShaderStageFlags(d.Stages), + } +} + +func (d *InputAttachment) BindingFlags() ext_descriptor_indexing.DescriptorBindingFlags { return 0 } + +func (d *InputAttachment) write() { + d.set.Write(core1_0.WriteDescriptorSet{ + DstSet: d.set.Ptr(), + DstBinding: d.binding, + DstArrayElement: 0, + DescriptorType: core1_0.DescriptorTypeInputAttachment, + ImageInfo: []core1_0.DescriptorImageInfo{ + { + ImageView: d.view, + ImageLayout: d.Layout, + }, + }, + }) +} diff --git a/engine/renderapi/descriptor/layout.go b/engine/renderapi/descriptor/layout.go new file mode 100644 index 0000000..f460f35 --- /dev/null +++ b/engine/renderapi/descriptor/layout.go @@ -0,0 +1,138 @@ +package descriptor + +import ( + "log" + + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/shader" + + "github.com/vkngwrapper/core/v2/common" + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/core/v2/driver" + "github.com/vkngwrapper/extensions/v2/ext_descriptor_indexing" +) + +type Map map[string]Descriptor + +type SetLayout interface { + device.Resource[core1_0.DescriptorSetLayout] + Name() string + Counts() map[core1_0.DescriptorType]int + VariableCount() int +} + +type SetLayoutTyped[S Set] interface { + SetLayout + Name() string + Instantiate(pool Pool) S +} + +type layout[S Set] struct { + device device.T + shader shader.T + ptr core1_0.DescriptorSetLayout + set S + allocated []Descriptor + maxCount int + counts map[core1_0.DescriptorType]int +} + +func New[S Set](device device.T, set S, shader shader.T) SetLayoutTyped[S] { + descriptors, err := ParseDescriptorStruct(set) + if err != nil { + panic(err) + } + + log.Println("descriptor set") + maxCount := 0 + createFlags := core1_0.DescriptorSetLayoutCreateFlags(0) + bindings := make([]core1_0.DescriptorSetLayoutBinding, 0, len(descriptors)) + bindFlags := make([]ext_descriptor_indexing.DescriptorBindingFlags, 0, len(descriptors)) + counts := make(map[core1_0.DescriptorType]int) + for name, descriptor := range descriptors { + index, exists := shader.Descriptor(name) + if !exists { + panic("unresolved descriptor") + } + binding := descriptor.LayoutBinding(index) + bindings = append(bindings, binding) + flags := descriptor.BindingFlags() + bindFlags = append(bindFlags, flags) + + if flags&ext_descriptor_indexing.DescriptorBindingUpdateAfterBind == ext_descriptor_indexing.DescriptorBindingUpdateAfterBind { + createFlags |= ext_descriptor_indexing.DescriptorSetLayoutCreateUpdateAfterBindPool + } + + if variable, ok := descriptor.(VariableDescriptor); ok { + maxCount = variable.MaxCount() + log.Printf(" %s -> %s x0-%d\n", name, descriptor, maxCount) + counts[binding.DescriptorType] = maxCount + } else { + log.Printf(" %s -> %s x%d\n", name, descriptor, binding.DescriptorCount) + counts[binding.DescriptorType] = binding.DescriptorCount + } + } + + bindFlagsInfo := ext_descriptor_indexing.DescriptorSetLayoutBindingFlagsCreateInfo{ + BindingFlags: bindFlags, + } + + info := core1_0.DescriptorSetLayoutCreateInfo{ + Flags: createFlags, + Bindings: bindings, + NextOptions: common.NextOptions{Next: bindFlagsInfo}, + } + + ptr, _, err := device.Ptr().CreateDescriptorSetLayout(nil, info) + if err != nil { + panic(err) + } + + device.SetDebugObjectName(driver.VulkanHandle(ptr.Handle()), core1_0.ObjectTypeDescriptorSetLayout, shader.Name()) + + return &layout[S]{ + device: device, + shader: shader, + ptr: ptr, + set: set, + maxCount: maxCount, + counts: counts, + } +} + +func (d *layout[S]) Name() string { + return d.shader.Name() +} + +func (d *layout[S]) Ptr() core1_0.DescriptorSetLayout { + return d.ptr +} + +func (d *layout[S]) Counts() map[core1_0.DescriptorType]int { + return d.counts +} + +func (d *layout[S]) VariableCount() int { + return d.maxCount +} + +func (d *layout[S]) Instantiate(pool Pool) S { + set := pool.Allocate(d) + copy, descriptors := CopyDescriptorStruct(d.set, set, d.shader) + for _, descriptor := range descriptors { + descriptor.Initialize(d.device) + d.allocated = append(d.allocated, descriptor) + } + return copy +} + +func (d *layout[S]) Destroy() { + // todo: allocated sets should probably clean up themselves + for _, desc := range d.allocated { + desc.Destroy() + } + if d.ptr != nil { + d.ptr.Destroy(nil) + d.ptr = nil + } +} diff --git a/engine/renderapi/descriptor/pool.go b/engine/renderapi/descriptor/pool.go new file mode 100644 index 0000000..0deafe0 --- /dev/null +++ b/engine/renderapi/descriptor/pool.go @@ -0,0 +1,115 @@ +package descriptor + +import ( + "log" + + "zworld/engine/renderapi/device" + + "github.com/vkngwrapper/core/v2/common" + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/core/v2/driver" + "github.com/vkngwrapper/extensions/v2/ext_descriptor_indexing" +) + +type Pool interface { + device.Resource[core1_0.DescriptorPool] + + Allocate(layouts SetLayout) Set + Recreate() +} + +type pool struct { + ptr core1_0.DescriptorPool + device device.T + sizes []core1_0.DescriptorPoolSize + maxSets int + + allocatedSets int + allocatedCounts map[core1_0.DescriptorType]int +} + +func NewPool(device device.T, sets int, sizes []core1_0.DescriptorPoolSize) Pool { + p := &pool{ + device: device, + ptr: nil, + sizes: sizes, + maxSets: sets, + allocatedCounts: make(map[core1_0.DescriptorType]int), + } + p.Recreate() + return p +} + +func (p *pool) Ptr() core1_0.DescriptorPool { + return p.ptr +} + +func (p *pool) Recreate() { + p.Destroy() + + info := core1_0.DescriptorPoolCreateInfo{ + Flags: ext_descriptor_indexing.DescriptorPoolCreateUpdateAfterBind, + PoolSizes: p.sizes, + MaxSets: p.maxSets, + } + ptr, result, err := p.device.Ptr().CreateDescriptorPool(nil, info) + if err != nil { + panic(err) + } + if result != core1_0.VKSuccess { + panic("failed to create descriptor pooll") + } + p.ptr = ptr +} + +func (p *pool) Destroy() { + if p.ptr == nil { + return + } + p.ptr.Destroy(nil) + p.ptr = nil +} + +func (p *pool) Allocate(layout SetLayout) Set { + info := core1_0.DescriptorSetAllocateInfo{ + DescriptorPool: p.ptr, + SetLayouts: []core1_0.DescriptorSetLayout{layout.Ptr()}, + } + + if layout.VariableCount() > 0 { + variableInfo := ext_descriptor_indexing.DescriptorSetVariableDescriptorCountAllocateInfo{ + DescriptorCounts: []int{layout.VariableCount()}, + } + info.NextOptions = common.NextOptions{Next: variableInfo} + } + + ptr, r, err := p.device.Ptr().AllocateDescriptorSets(info) + if err != nil { + log.Println("allocated sets:", p.allocatedSets, "/", p.maxSets) + log.Println("allocated counts:", p.allocatedCounts) + panic(err) + } + if r != core1_0.VKSuccess { + if r == core1_0.VKErrorOutOfDeviceMemory { + panic("failed to allocate descriptor set: out of pool memory") + } + panic("failed to allocate descriptor set") + } + + p.device.SetDebugObjectName( + driver.VulkanHandle(ptr[0].Handle()), + core1_0.ObjectTypeDescriptorSet, + layout.Name()) + + p.allocatedSets++ + for kind, count := range layout.Counts() { + current, _ := p.allocatedCounts[kind] + p.allocatedCounts[kind] = current + count + } + + return &set{ + device: p.device, + ptr: ptr[0], + layout: layout, + } +} diff --git a/engine/renderapi/descriptor/sampler.go b/engine/renderapi/descriptor/sampler.go new file mode 100644 index 0000000..db30e08 --- /dev/null +++ b/engine/renderapi/descriptor/sampler.go @@ -0,0 +1,69 @@ +package descriptor + +import ( + "fmt" + + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/texture" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/extensions/v2/ext_descriptor_indexing" +) + +type Sampler struct { + Stages core1_0.ShaderStageFlags + + binding int + sampler core1_0.Sampler + view core1_0.ImageView + set Set +} + +var _ Descriptor = &Sampler{} + +func (d *Sampler) Initialize(device device.T) {} + +func (d *Sampler) String() string { + return fmt.Sprintf("Sampler:%d", d.binding) +} + +func (d *Sampler) Destroy() {} + +func (d *Sampler) Bind(set Set, binding int) { + d.set = set + d.binding = binding +} + +func (d *Sampler) Set(tex texture.T) { + d.sampler = tex.Ptr() + d.view = tex.View().Ptr() + d.write() +} + +func (d *Sampler) LayoutBinding(binding int) core1_0.DescriptorSetLayoutBinding { + d.binding = binding + return core1_0.DescriptorSetLayoutBinding{ + Binding: binding, + DescriptorType: core1_0.DescriptorTypeCombinedImageSampler, + DescriptorCount: 1, + StageFlags: core1_0.ShaderStageFlags(d.Stages), + } +} + +func (d *Sampler) BindingFlags() ext_descriptor_indexing.DescriptorBindingFlags { return 0 } + +func (d *Sampler) write() { + d.set.Write(core1_0.WriteDescriptorSet{ + DstSet: d.set.Ptr(), + DstBinding: d.binding, + DstArrayElement: 0, + DescriptorType: core1_0.DescriptorTypeCombinedImageSampler, + ImageInfo: []core1_0.DescriptorImageInfo{ + { + Sampler: d.sampler, + ImageView: d.view, + ImageLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + }, + }, + }) +} diff --git a/engine/renderapi/descriptor/sampler_array.go b/engine/renderapi/descriptor/sampler_array.go new file mode 100644 index 0000000..c5dc318 --- /dev/null +++ b/engine/renderapi/descriptor/sampler_array.go @@ -0,0 +1,125 @@ +package descriptor + +import ( + "fmt" + + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/texture" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/extensions/v2/ext_descriptor_indexing" +) + +type SamplerArray struct { + Count int + Stages core1_0.ShaderStageFlags + + binding int + sampler []core1_0.Sampler + view []core1_0.ImageView + set Set + + // re-used update arrays + info []core1_0.DescriptorImageInfo + writes []core1_0.WriteDescriptorSet +} + +var _ Descriptor = &SamplerArray{} + +func (d *SamplerArray) Initialize(device device.T) { + if d.Count == 0 { + panic("sampler array has count 0") + } + + d.sampler = make([]core1_0.Sampler, d.Count) + d.view = make([]core1_0.ImageView, d.Count) + d.info = make([]core1_0.DescriptorImageInfo, 0, d.Count) + d.writes = make([]core1_0.WriteDescriptorSet, 0, 100) +} + +func (d *SamplerArray) String() string { + return fmt.Sprintf("SamplerArray[%d]:%d", d.Count, d.binding) +} + +func (d *SamplerArray) Destroy() {} + +func (d *SamplerArray) Bind(set Set, binding int) { + d.set = set + d.binding = binding +} + +func (d *SamplerArray) LayoutBinding(binding int) core1_0.DescriptorSetLayoutBinding { + d.binding = binding + return core1_0.DescriptorSetLayoutBinding{ + Binding: binding, + DescriptorType: core1_0.DescriptorTypeCombinedImageSampler, + DescriptorCount: d.Count, + StageFlags: d.Stages, + } +} + +func (d *SamplerArray) BindingFlags() ext_descriptor_indexing.DescriptorBindingFlags { + return ext_descriptor_indexing.DescriptorBindingVariableDescriptorCount | + ext_descriptor_indexing.DescriptorBindingPartiallyBound | + ext_descriptor_indexing.DescriptorBindingUpdateAfterBind | + ext_descriptor_indexing.DescriptorBindingUpdateUnusedWhilePending +} + +func (d *SamplerArray) MaxCount() int { + return d.Count +} + +func (d *SamplerArray) Set(index int, tex texture.T) { + if index > d.Count { + panic("out of bounds") + } + if tex == nil { + panic("texture is null") + } + d.sampler[index] = tex.Ptr() + d.view[index] = tex.View().Ptr() + d.write(index, 1) +} + +func (d *SamplerArray) Clear(index int) { + if index > d.Count { + panic("out of bounds") + } + d.sampler[index] = nil + d.view[index] = nil + d.write(index, 1) +} + +func (d *SamplerArray) SetRange(textures []texture.T, offset int) { + end := offset + len(textures) + if end > d.Count { + panic("out of bounds") + } + for i, tex := range textures { + if tex == nil { + panic(fmt.Sprintf("texture[%d] is null", i)) + } + d.sampler[offset+i] = tex.Ptr() + d.view[offset+i] = tex.View().Ptr() + } + d.write(offset, len(textures)) +} + +func (d *SamplerArray) write(index, count int) { + images := make([]core1_0.DescriptorImageInfo, count) + for i := range images { + images[i] = core1_0.DescriptorImageInfo{ + Sampler: d.sampler[index+i], + ImageView: d.view[index+i], + ImageLayout: core1_0.ImageLayoutShaderReadOnlyOptimal, + } + } + + d.set.Write(core1_0.WriteDescriptorSet{ + DstSet: d.set.Ptr(), + DstBinding: d.binding, + DstArrayElement: index, + DescriptorType: core1_0.DescriptorTypeCombinedImageSampler, + ImageInfo: images, + }) +} diff --git a/engine/renderapi/descriptor/set.go b/engine/renderapi/descriptor/set.go new file mode 100644 index 0000000..a91d099 --- /dev/null +++ b/engine/renderapi/descriptor/set.go @@ -0,0 +1,29 @@ +package descriptor + +import ( + "zworld/engine/renderapi/device" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +type Set interface { + Ptr() core1_0.DescriptorSet + Write(write core1_0.WriteDescriptorSet) +} + +type set struct { + device device.T + layout SetLayout + ptr core1_0.DescriptorSet +} + +func (s *set) Ptr() core1_0.DescriptorSet { + return s.ptr +} + +func (s *set) Write(write core1_0.WriteDescriptorSet) { + write.DstSet = s.ptr + if err := s.device.Ptr().UpdateDescriptorSets([]core1_0.WriteDescriptorSet{write}, nil); err != nil { + panic(err) + } +} diff --git a/engine/renderapi/descriptor/set_mock.go b/engine/renderapi/descriptor/set_mock.go new file mode 100644 index 0000000..29bb238 --- /dev/null +++ b/engine/renderapi/descriptor/set_mock.go @@ -0,0 +1,17 @@ +package descriptor + +import ( + "github.com/vkngwrapper/core/v2/core1_0" +) + +type SetMock struct { +} + +var _ Set = &SetMock{} + +func (s *SetMock) Ptr() core1_0.DescriptorSet { + return nil +} + +func (s *SetMock) Write(write core1_0.WriteDescriptorSet) { +} diff --git a/engine/renderapi/descriptor/storage.go b/engine/renderapi/descriptor/storage.go new file mode 100644 index 0000000..61c6d41 --- /dev/null +++ b/engine/renderapi/descriptor/storage.go @@ -0,0 +1,92 @@ +package descriptor + +import ( + "fmt" + "reflect" + + "zworld/engine/renderapi/buffer" + "zworld/engine/renderapi/device" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/extensions/v2/ext_descriptor_indexing" +) + +type Storage[K comparable] struct { + Stages core1_0.ShaderStageFlags + Size int + + binding int + buffer buffer.Array[K] + set Set +} + +func (d *Storage[K]) Initialize(device device.T) { + if d.set == nil { + panic("descriptor must be bound first") + } + if d.Size == 0 { + panic("storage descriptor size must be non-zero") + } + + d.buffer = buffer.NewArray[K](device, buffer.Args{ + Key: d.String(), + Size: d.Size, + Usage: core1_0.BufferUsageStorageBuffer, + Memory: core1_0.MemoryPropertyDeviceLocal | core1_0.MemoryPropertyHostVisible, + }) + d.write() +} + +func (d *Storage[K]) String() string { + var empty K + kind := reflect.TypeOf(empty) + return fmt.Sprintf("Storage[%s]:%d", kind.Name(), d.binding) +} + +func (d *Storage[K]) Destroy() { + if d.buffer != nil { + d.buffer.Destroy() + d.buffer = nil + } +} + +func (d *Storage[K]) Bind(set Set, binding int) { + d.set = set + d.binding = binding +} + +func (d *Storage[K]) Set(index int, data K) { + d.buffer.Set(index, data) +} + +func (d *Storage[K]) SetRange(offset int, data []K) { + d.buffer.SetRange(offset, data) +} + +func (d *Storage[K]) LayoutBinding(binding int) core1_0.DescriptorSetLayoutBinding { + d.binding = binding + return core1_0.DescriptorSetLayoutBinding{ + Binding: binding, + DescriptorType: core1_0.DescriptorTypeStorageBuffer, + DescriptorCount: 1, + StageFlags: core1_0.ShaderStageFlags(d.Stages), + } +} + +func (d *Storage[K]) BindingFlags() ext_descriptor_indexing.DescriptorBindingFlags { return 0 } + +func (d *Storage[K]) write() { + d.set.Write(core1_0.WriteDescriptorSet{ + DstSet: d.set.Ptr(), + DstBinding: d.binding, + DstArrayElement: 0, + DescriptorType: core1_0.DescriptorTypeStorageBuffer, + BufferInfo: []core1_0.DescriptorBufferInfo{ + { + Buffer: d.buffer.Ptr(), + Offset: 0, + Range: d.buffer.Size(), + }, + }, + }) +} diff --git a/engine/renderapi/descriptor/uniform.go b/engine/renderapi/descriptor/uniform.go new file mode 100644 index 0000000..c3783e9 --- /dev/null +++ b/engine/renderapi/descriptor/uniform.go @@ -0,0 +1,80 @@ +package descriptor + +import ( + "fmt" + "reflect" + + "zworld/engine/renderapi/buffer" + "zworld/engine/renderapi/device" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/extensions/v2/ext_descriptor_indexing" +) + +type Uniform[K any] struct { + Stages core1_0.ShaderStageFlags + + binding int + buffer buffer.Item[K] + set Set +} + +func (d *Uniform[K]) Initialize(device device.T) { + if d.set == nil { + panic("descriptor must be bound first") + } + d.buffer = buffer.NewItem[K](device, buffer.Args{ + Usage: core1_0.BufferUsageUniformBuffer, + Memory: core1_0.MemoryPropertyDeviceLocal | core1_0.MemoryPropertyHostVisible, + }) + d.write() +} + +func (d *Uniform[K]) String() string { + var empty K + kind := reflect.TypeOf(empty) + return fmt.Sprintf("Uniform[%s]:%d", kind.Name(), d.binding) +} + +func (d *Uniform[K]) Destroy() { + if d.buffer != nil { + d.buffer.Destroy() + d.buffer = nil + } +} + +func (d *Uniform[K]) Bind(set Set, binding int) { + d.set = set + d.binding = binding +} + +func (d *Uniform[K]) Set(data K) { + d.buffer.Set(data) +} + +func (d *Uniform[K]) write() { + d.set.Write(core1_0.WriteDescriptorSet{ + DstBinding: d.binding, + DstArrayElement: 0, + DescriptorType: core1_0.DescriptorTypeUniformBuffer, + BufferInfo: []core1_0.DescriptorBufferInfo{ + { + Buffer: d.buffer.Ptr(), + Offset: 0, + Range: d.buffer.Size(), + }, + }, + }) +} + +func (d *Uniform[K]) LayoutBinding(binding int) core1_0.DescriptorSetLayoutBinding { + d.binding = binding + return core1_0.DescriptorSetLayoutBinding{ + Binding: binding, + DescriptorType: core1_0.DescriptorTypeUniformBuffer, + DescriptorCount: 1, + StageFlags: core1_0.ShaderStageFlags(d.Stages), + } +} + +func (d *Uniform[K]) BindingFlags() ext_descriptor_indexing.DescriptorBindingFlags { return 0 } diff --git a/engine/renderapi/descriptor/uniform_array.go b/engine/renderapi/descriptor/uniform_array.go new file mode 100644 index 0000000..0186703 --- /dev/null +++ b/engine/renderapi/descriptor/uniform_array.go @@ -0,0 +1,88 @@ +package descriptor + +import ( + "fmt" + "reflect" + + "zworld/engine/renderapi/buffer" + "zworld/engine/renderapi/device" + "zworld/engine/util" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/extensions/v2/ext_descriptor_indexing" +) + +type UniformArray[K any] struct { + Size int + Stages core1_0.ShaderStageFlags + + binding int + buffer buffer.Array[K] + set Set +} + +func (d *UniformArray[K]) Initialize(device device.T) { + if d.set == nil { + panic("descriptor must be bound first") + } + d.buffer = buffer.NewArray[K](device, buffer.Args{ + Key: d.String(), + Size: d.Size, + Usage: core1_0.BufferUsageUniformBuffer, + Memory: core1_0.MemoryPropertyDeviceLocal | core1_0.MemoryPropertyHostVisible, + }) + d.write() +} + +func (d *UniformArray[K]) String() string { + var empty K + kind := reflect.TypeOf(empty) + return fmt.Sprintf("UniformArray[%s]:%d", kind.Name(), d.binding) +} + +func (d *UniformArray[K]) Destroy() { + if d.buffer != nil { + d.buffer.Destroy() + d.buffer = nil + } +} + +func (d *UniformArray[K]) Bind(set Set, binding int) { + d.set = set + d.binding = binding +} + +func (d *UniformArray[K]) Set(index int, data K) { + d.buffer.Set(index, data) +} + +func (d *UniformArray[K]) SetRange(offset int, data []K) { + d.buffer.SetRange(offset, data) +} + +func (d *UniformArray[K]) write() { + d.set.Write(core1_0.WriteDescriptorSet{ + DstBinding: d.binding, + DstArrayElement: 0, + DescriptorType: core1_0.DescriptorTypeUniformBuffer, + BufferInfo: util.Map(util.Range(0, d.Size, 1), func(i int) core1_0.DescriptorBufferInfo { + return core1_0.DescriptorBufferInfo{ + Buffer: d.buffer.Ptr(), + Offset: i * d.buffer.Element(), + Range: d.buffer.Element(), + } + }), + }) +} + +func (d *UniformArray[K]) LayoutBinding(binding int) core1_0.DescriptorSetLayoutBinding { + d.binding = binding + return core1_0.DescriptorSetLayoutBinding{ + Binding: binding, + DescriptorType: core1_0.DescriptorTypeUniformBuffer, + DescriptorCount: d.Size, + StageFlags: core1_0.ShaderStageFlags(d.Stages), + } +} + +func (d *UniformArray[K]) BindingFlags() ext_descriptor_indexing.DescriptorBindingFlags { return 0 } diff --git a/engine/renderapi/device/device.go b/engine/renderapi/device/device.go new file mode 100644 index 0000000..783e05b --- /dev/null +++ b/engine/renderapi/device/device.go @@ -0,0 +1,174 @@ +package device + +import ( + "log" + + "zworld/engine/renderapi/vulkan/instance" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/core/v2/driver" + "github.com/vkngwrapper/extensions/v2/ext_debug_utils" +) + +type Resource[T any] interface { + Destroy() + Ptr() T +} + +type T interface { + Resource[core1_0.Device] + + Physical() core1_0.PhysicalDevice + Allocate(key string, req core1_0.MemoryRequirements, flags core1_0.MemoryPropertyFlags) Memory + GetQueue(queueIndex int, flags core1_0.QueueFlags) core1_0.Queue + GetQueueFamilyIndex(flags core1_0.QueueFlags) int + GetDepthFormat() core1_0.Format + GetMemoryTypeIndex(uint32, core1_0.MemoryPropertyFlags) int + GetLimits() *core1_0.PhysicalDeviceLimits + WaitIdle() + + SetDebugObjectName(ptr driver.VulkanHandle, objType core1_0.ObjectType, name string) +} + +type device struct { + physical core1_0.PhysicalDevice + ptr core1_0.Device + limits *core1_0.PhysicalDeviceLimits + debug ext_debug_utils.Extension + + memtypes map[memtype]int + queues map[core1_0.QueueFlags]int +} + +func New(instance instance.T, physDevice core1_0.PhysicalDevice) (T, error) { + log.Println("creating device with extensions", deviceExtensions) + + families := physDevice.QueueFamilyProperties() + log.Println("Queue families:", len(families)) + for index, family := range families { + log.Printf(" [%d,%d]: %d\n", index, family.QueueCount, family.QueueFlags) + } + + dev, _, err := physDevice.CreateDevice(nil, core1_0.DeviceCreateInfo{ + NextOptions: _NextOptions(), + EnabledExtensionNames: _DeviceExtension(), + QueueCreateInfos: _QueueCreateInfos(families), + EnabledFeatures: _EnabledFeatures(), + }) + if err != nil { + return nil, err + } + + properties, err := physDevice.Properties() + if err != nil { + return nil, err + } + log.Println("minimum uniform buffer alignment:", properties.Limits.MinUniformBufferOffsetAlignment) + log.Println("minimum storage buffer alignment:", properties.Limits.MinStorageBufferOffsetAlignment) + + debug := ext_debug_utils.CreateExtensionFromInstance(instance.Ptr()) + + return &device{ + ptr: dev, + debug: debug, + physical: physDevice, + limits: properties.Limits, + memtypes: make(map[memtype]int), + queues: make(map[core1_0.QueueFlags]int), + }, nil +} + +func (d *device) Ptr() core1_0.Device { + return d.ptr +} + +func (d *device) Physical() core1_0.PhysicalDevice { + return d.physical +} + +func (d *device) GetQueue(queueIndex int, flags core1_0.QueueFlags) core1_0.Queue { + return d.ptr.GetQueue(queueIndex, 0) +} + +func (d *device) GetQueueFamilyIndex(flags core1_0.QueueFlags) int { + if q, ok := d.queues[flags]; ok { + return q + } + + families := d.physical.QueueFamilyProperties() + for index, family := range families { + if family.QueueFlags&flags == flags { + d.queues[flags] = index + return index + } + } + + panic("no such queue available") +} + +func (d *device) GetDepthFormat() core1_0.Format { + depthFormats := []core1_0.Format{ + core1_0.FormatD32SignedFloatS8UnsignedInt, + core1_0.FormatD32SignedFloat, + core1_0.FormatD24UnsignedNormalizedS8UnsignedInt, + core1_0.FormatD16UnsignedNormalizedS8UnsignedInt, + core1_0.FormatD16UnsignedNormalized, + } + for _, format := range depthFormats { + props := d.physical.FormatProperties(format) + + if props.OptimalTilingFeatures&core1_0.FormatFeatureDepthStencilAttachment == core1_0.FormatFeatureDepthStencilAttachment { + return format + } + } + return depthFormats[0] +} + +func (d *device) GetMemoryTypeIndex(typeBits uint32, flags core1_0.MemoryPropertyFlags) int { + mtype := memtype{typeBits, flags} + if t, ok := d.memtypes[mtype]; ok { + return t + } + + props := d.physical.MemoryProperties() + for i, kind := range props.MemoryTypes { + if typeBits&1 == 1 { + if kind.PropertyFlags&flags == flags { + d.memtypes[mtype] = i + return i + } + } + typeBits >>= 1 + } + + d.memtypes[mtype] = 0 + return 0 +} + +func (d *device) GetLimits() *core1_0.PhysicalDeviceLimits { + return d.limits +} + +func (d *device) Allocate(key string, req core1_0.MemoryRequirements, flags core1_0.MemoryPropertyFlags) Memory { + if req.Size == 0 { + panic("allocating 0 bytes of memory") + } + return alloc(d, key, req, flags) +} + +func (d *device) Destroy() { + d.ptr.Destroy(nil) + d.ptr = nil +} + +func (d *device) WaitIdle() { + d.ptr.WaitIdle() +} + +func (d *device) SetDebugObjectName(handle driver.VulkanHandle, objType core1_0.ObjectType, name string) { + d.debug.SetDebugUtilsObjectName(d.ptr, ext_debug_utils.DebugUtilsObjectNameInfo{ + ObjectName: name, + ObjectHandle: handle, + ObjectType: objType, + }) +} diff --git a/engine/renderapi/device/device_help.go b/engine/renderapi/device/device_help.go new file mode 100644 index 0000000..386cdb7 --- /dev/null +++ b/engine/renderapi/device/device_help.go @@ -0,0 +1,52 @@ +package device + +import ( + "github.com/vkngwrapper/core/v2/common" + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/extensions/v2/ext_descriptor_indexing" + "github.com/vkngwrapper/extensions/v2/khr_swapchain" +) + +var deviceExtensions = []string{ + khr_swapchain.ExtensionName, +} + +func _DeviceExtension() []string { + return deviceExtensions +} + +// todo: check QueueFamilyProperty; +func _QueueCreateInfos(families []*core1_0.QueueFamilyProperties) []core1_0.DeviceQueueCreateInfo { + var queueFamilyOptions []core1_0.DeviceQueueCreateInfo + for k, _ := range families { + queueFamilyOptions = append(queueFamilyOptions, core1_0.DeviceQueueCreateInfo{ + QueueFamilyIndex: k, + QueuePriorities: []float32{1}, + }) + } + return queueFamilyOptions +} + +// todo: what's means the nextoption? +func _NextOptions() common.NextOptions { + indexingFeatures := ext_descriptor_indexing.PhysicalDeviceDescriptorIndexingFeatures{ + ShaderSampledImageArrayNonUniformIndexing: true, + RuntimeDescriptorArray: true, + DescriptorBindingPartiallyBound: true, + DescriptorBindingVariableDescriptorCount: true, + DescriptorBindingUpdateUnusedWhilePending: true, + DescriptorBindingUniformBufferUpdateAfterBind: true, + DescriptorBindingSampledImageUpdateAfterBind: true, + DescriptorBindingStorageBufferUpdateAfterBind: true, + DescriptorBindingStorageTexelBufferUpdateAfterBind: true, + } + return common.NextOptions{Next: indexingFeatures} +} + +// todo: check DeviceFeature and what's means; +func _EnabledFeatures() *core1_0.PhysicalDeviceFeatures { + return &core1_0.PhysicalDeviceFeatures{ + IndependentBlend: true, + DepthClamp: true, + } +} diff --git a/engine/renderapi/device/memcpy.go b/engine/renderapi/device/memcpy.go new file mode 100644 index 0000000..0ec93f6 --- /dev/null +++ b/engine/renderapi/device/memcpy.go @@ -0,0 +1,8 @@ +package device + +import "unsafe" + +func Memcpy(dst, src unsafe.Pointer, n int) int { + copy(unsafe.Slice((*byte)(dst), n), unsafe.Slice((*byte)(src), n)) + return n +} diff --git a/engine/renderapi/device/memory.go b/engine/renderapi/device/memory.go new file mode 100644 index 0000000..126ea54 --- /dev/null +++ b/engine/renderapi/device/memory.go @@ -0,0 +1,211 @@ +package device + +import ( + "fmt" + "reflect" + "unsafe" + "zworld/engine/util" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/core/v2/driver" +) + +type Memory interface { + Resource[core1_0.DeviceMemory] + Read(offset int, data any) int + Write(offset int, data any) int + Flush() + Invalidate() +} + +type memtype struct { + TypeBits uint32 + Flags core1_0.MemoryPropertyFlags +} + +type memory struct { + ptr core1_0.DeviceMemory + device T + size int + flags core1_0.MemoryPropertyFlags + mapPtr unsafe.Pointer +} + +func alloc(device T, key string, req core1_0.MemoryRequirements, flags core1_0.MemoryPropertyFlags) Memory { + typeIdx := device.GetMemoryTypeIndex(req.MemoryTypeBits, flags) + + align := int(device.GetLimits().NonCoherentAtomSize) + size := util.Align(int(req.Size), align) + + ptr, _, err := device.Ptr().AllocateMemory(nil, core1_0.MemoryAllocateInfo{ + AllocationSize: size, + MemoryTypeIndex: typeIdx, + }) + if err != nil { + panic(fmt.Sprintf("failed to allocate %d bytes of memory: %s", req.Size, err)) + } + + if key != "" { + device.SetDebugObjectName(driver.VulkanHandle(ptr.Handle()), + core1_0.ObjectTypeDeviceMemory, key) + } + + return &memory{ + device: device, + ptr: ptr, + flags: flags, + size: size, + } +} + +func (m *memory) isHostVisible() bool { + bit := core1_0.MemoryPropertyHostVisible + return m.flags&bit == bit +} + +func (m *memory) isCoherent() bool { + bit := core1_0.MemoryPropertyHostCoherent + return m.flags&bit == bit +} + +func (m *memory) Ptr() core1_0.DeviceMemory { + return m.ptr +} + +func (m *memory) Destroy() { + m.unmap() + m.ptr.Free(nil) + m.ptr = nil +} + +func (m *memory) mmap() { + var nullPtr unsafe.Pointer + if m.mapPtr != nullPtr { + // already mapped + return + } + var dst unsafe.Pointer + dst, _, err := m.ptr.Map(0, -1, 0) + if err != nil { + panic(err) + } + m.mapPtr = dst +} + +func (m *memory) unmap() { + var nullPtr unsafe.Pointer + if m.mapPtr == nullPtr { + // already unmapped + return + } + m.ptr.Unmap() + m.mapPtr = nullPtr +} + +func (m *memory) Write(offset int, data any) int { + if m.ptr == nil { + panic("write to freed memory block") + } + if !m.isHostVisible() { + panic("memory is not visible to host") + } + + size := 0 + var src unsafe.Pointer + + t := reflect.TypeOf(data) + v := reflect.ValueOf(data) + + if t.Kind() == reflect.Slice { + // calculate copy size + count := v.Len() + sizeof := int(t.Elem().Size()) + size = count * sizeof + + // get a pointer to the beginning of the array + src = unsafe.Pointer(v.Pointer()) + } else if t.Kind() == reflect.Pointer { + src = v.UnsafePointer() + size = int(v.Elem().Type().Size()) + } else { + panic(fmt.Errorf("buffered data must be a slice, struct or a pointer")) + } + + if offset < 0 || offset+size > m.size { + panic("out of bounds") + } + + // map shared memory + m.mmap() + + // create pointer at offset + offsetDst := unsafe.Pointer(uintptr(m.mapPtr) + uintptr(offset)) + + // copy from host + Memcpy(offsetDst, src, size) + + // flush region + // todo: optimize to the smallest possible region + // m.Flush() + + // unmap shared memory + // m.ptr.Unmap() + + return size +} + +func (m *memory) Read(offset int, target any) int { + if m.ptr == nil { + panic("read from freed memory block") + } + if !m.isHostVisible() { + panic("memory is not visible to host") + } + + size := 0 + var dst unsafe.Pointer + + t := reflect.TypeOf(target) + v := reflect.ValueOf(target) + + if t.Kind() == reflect.Slice { + // calculate copy size + count := v.Len() + sizeof := int(t.Elem().Size()) + size = count * sizeof + + // get a pointer to the beginning of the array + dst = unsafe.Pointer(v.Pointer()) + } else if t.Kind() == reflect.Pointer { + dst = v.UnsafePointer() + size = int(v.Elem().Type().Size()) + } else { + panic(fmt.Errorf("buffered data must be a slice, struct or a pointer")) + } + + if size+offset > m.size { + panic("out of bounds") + } + + // map shared memory + m.mmap() + + // copy to host + offsetPtr := unsafe.Pointer(uintptr(m.mapPtr) + uintptr(offset)) + Memcpy(dst, offsetPtr, size) + + // unmap shared memory + // m.ptr.Unmap() + + return size +} + +func (m *memory) Flush() { + if !m.isCoherent() { + m.ptr.FlushAll() + } +} + +func (m *memory) Invalidate() { + m.ptr.InvalidateAll() +} diff --git a/engine/renderapi/font/font.go b/engine/renderapi/font/font.go new file mode 100644 index 0000000..fc9549d --- /dev/null +++ b/engine/renderapi/font/font.go @@ -0,0 +1,180 @@ +package font + +import ( + "errors" + "fmt" + "sync" + + "zworld/engine/renderapi/color" + "zworld/engine/renderapi/image" + "zworld/engine/util" + "zworld/plugins/math" + "zworld/plugins/math/vec2" + + fontlib "golang.org/x/image/font" + "golang.org/x/image/math/fixed" +) + +var ErrNoGlyph = errors.New("no glyph for rune") + +type T interface { + Name() string + Glyph(rune) (*Glyph, error) + Kern(rune, rune) float32 + Measure(string, Args) vec2.T + Size() float32 +} + +type Args struct { + Color color.T + LineHeight float32 +} + +type font struct { + size float32 + scale float32 + name string + face fontlib.Face + drawer *fontlib.Drawer + mutex *sync.Mutex + glyphs *util.SyncMap[rune, *Glyph] + kern *util.SyncMap[runepair, float32] +} + +type runepair struct { + a, b rune +} + +func (f *font) Name() string { return f.name } +func (f *font) Size() float32 { return f.size } + +func (f *font) Glyph(r rune) (*Glyph, error) { + if cached, exists := f.glyphs.Load(r); exists { + return cached, nil + } + + // grab the font lock + f.mutex.Lock() + defer f.mutex.Unlock() + + bounds, advance, ok := f.face.GlyphBounds(r) + if !ok { + return nil, ErrNoGlyph + } + + // calculate bearing + bearing := vec2.New(FixToFloat(bounds.Min.X), FixToFloat(bounds.Min.Y)) + + // texture size + size := vec2.New(FixToFloat(bounds.Max.X), FixToFloat(bounds.Max.Y)).Sub(bearing) + + // glyph texture + _, mask, offset, _, _ := f.face.Glyph(fixed.Point26_6{X: 0, Y: 0}, r) + width, height := int(size.X), int(size.Y) + + img := &image.Data{ + Width: width, + Height: height, + Buffer: make([]byte, 4*width*height), + Format: image.FormatRGBA8Unorm, + } + i := 0 + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + // grab alpha value as 16-bit integer + _, _, _, alpha := mask.At(offset.X+x, offset.Y+y).RGBA() + img.Buffer[i+0] = 0xFF // red + img.Buffer[i+1] = 0xFF // green + img.Buffer[i+2] = 0xFF // blue + img.Buffer[i+3] = uint8(alpha >> 8) + i += 4 + } + } + + scaleFactor := 1 / f.scale + glyph := &Glyph{ + key: fmt.Sprintf("glyph:%s:%dx%.2f:%c", f.Name(), int(f.size), f.scale, r), + Size: size.Scaled(scaleFactor), + Bearing: bearing.Scaled(scaleFactor), + Advance: FixToFloat(advance) * scaleFactor, + Mask: img, + } + f.glyphs.Store(r, glyph) + + return glyph, nil +} + +func (f *font) Kern(a, b rune) float32 { + pair := runepair{a, b} + if k, exists := f.kern.Load(pair); exists { + return k + } + + f.mutex.Lock() + defer f.mutex.Unlock() + k := FixToFloat(f.face.Kern(a, b)) + f.kern.Store(pair, k) + return k +} + +func (f *font) MeasureLine(text string) vec2.T { + size := vec2.Zero + var prev rune + for i, r := range text { + g, err := f.Glyph(r) + if err != nil { + panic("no such glyph") + } + if i > 0 { + size.X += f.Kern(prev, r) + } + if i < len(text)-1 { + size.X += g.Advance + } else { + size.X += g.Bearing.X + g.Size.X + } + size.Y = math.Max(size.Y, g.Size.Y) + prev = r + } + return size.Scaled(f.scale) +} + +func (f *font) Measure(text string, args Args) vec2.T { + if args.LineHeight == 0 { + args.LineHeight = 1 + } + + lines := 1 + width := float32(0) + s := 0 + for i, c := range text { + if c == '\n' { + line := text[s:i] + // w := f.drawer.MeasureString(line).Ceil() + w := f.MeasureLine(line) + if w.X > width { + width = w.X + } + s = i + 1 + lines++ + } + } + r := len(text) + if s < r { + line := text[s:] + // w := f.drawer.MeasureString(line).Ceil() + w := f.MeasureLine(line) + if w.X > width { + width = w.X + } + } + + lineHeight := int(math.Ceil(f.size * f.scale * args.LineHeight)) + height := lineHeight*lines + (lineHeight/2)*(lines-1) + return vec2.New(width, float32(height)).Scaled(1 / f.scale).Ceil() +} + +func FixToFloat(v fixed.Int26_6) float32 { + const scalar = 1 / float32(1<<6) + return float32(v) * scalar +} diff --git a/engine/renderapi/font/font_suite_test.go b/engine/renderapi/font/font_suite_test.go new file mode 100644 index 0000000..59b238c --- /dev/null +++ b/engine/renderapi/font/font_suite_test.go @@ -0,0 +1,33 @@ +package font_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "golang.org/x/image/math/fixed" + "zworld/engine/renderapi/font" +) + +func TestFont(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "renderapi/font") +} + +var _ = Describe("font utils", func() { + It("converts fixed to float32", func() { + v := fixed.I(2) + Expect(font.FixToFloat(v)).To(BeNumerically("~", float32(2.0))) + + v2 := fixed.Int26_6(1<<6 + 1<<4) + Expect(font.FixToFloat(v2)).To(BeNumerically("~", float32(1.25))) + }) + + It("extracts glyphs", func() { + f := font.Load("fonts/SourceSansPro-Regular.ttf", 12, 1) + Expect(f).ToNot(BeNil()) + a, err := f.Glyph('g') + Expect(err).ToNot(HaveOccurred()) + Expect(a.Advance).To(BeNumerically(">", 0)) + }) +}) diff --git a/engine/renderapi/font/glyph.go b/engine/renderapi/font/glyph.go new file mode 100644 index 0000000..eb6b970 --- /dev/null +++ b/engine/renderapi/font/glyph.go @@ -0,0 +1,34 @@ +package font + +import ( + "zworld/engine/renderapi/image" + "zworld/engine/renderapi/texture" + "zworld/plugins/math/vec2" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +type Glyph struct { + key string + Size vec2.T + Bearing vec2.T + Advance float32 + Mask *image.Data +} + +var _ texture.Ref = &Glyph{} + +func (r *Glyph) Key() string { return r.key } +func (r *Glyph) Version() int { return 1 } + +func (r *Glyph) ImageData() *image.Data { + return r.Mask +} + +func (r *Glyph) TextureArgs() texture.Args { + return texture.Args{ + Filter: texture.FilterNearest, + Wrap: texture.WrapClamp, + Border: core1_0.BorderColorFloatTransparentBlack, + } +} diff --git a/engine/renderapi/font/loader.go b/engine/renderapi/font/loader.go new file mode 100644 index 0000000..48c1a09 --- /dev/null +++ b/engine/renderapi/font/loader.go @@ -0,0 +1,75 @@ +package font + +import ( + "fmt" + "log" + "sync" + + "github.com/golang/freetype/truetype" + fontlib "golang.org/x/image/font" + + "zworld/assets" + "zworld/engine/util" +) + +var parseCache map[string]*truetype.Font = make(map[string]*truetype.Font, 32) +var faceCache map[string]T = make(map[string]T, 128) + +func loadTruetypeFont(filename string) (*truetype.Font, error) { + // check parsed font cache + if fnt, exists := parseCache[filename]; exists { + return fnt, nil + } + + fontBytes, err := assets.ReadAll(filename) + if err != nil { + return nil, fmt.Errorf("failed to load font: %w", err) + } + + fnt, err := truetype.Parse(fontBytes) + if err != nil { + return nil, fmt.Errorf("failed to parse font: %w", err) + } + + // add to cache + parseCache[filename] = fnt + return fnt, nil +} + +func Load(filename string, size int, scale float32) T { + key := fmt.Sprintf("%s:%dx%.2f", filename, size, scale) + if font, exists := faceCache[key]; exists { + return font + } + + ttf, err := loadTruetypeFont(filename) + if err != nil { + panic(err) + } + + name := ttf.Name(truetype.NameIDFontFullName) + log.Printf("+ font %s %dpt x%.2f\n", name, size, scale) + + dpi := 72.0 * scale + face := truetype.NewFace(ttf, &truetype.Options{ + Size: float64(size), + DPI: float64(dpi), + Hinting: fontlib.HintingFull, + SubPixelsX: 8, + SubPixelsY: 8, + }) + + fnt := &font{ + size: float32(size), + scale: scale, + name: name, + face: face, + glyphs: util.NewSyncMap[rune, *Glyph](), + kern: util.NewSyncMap[runepair, float32](), + drawer: &fontlib.Drawer{Face: face}, + mutex: &sync.Mutex{}, + } + + faceCache[key] = fnt + return fnt +} diff --git a/engine/renderapi/framebuffer/array.go b/engine/renderapi/framebuffer/array.go new file mode 100644 index 0000000..dca4449 --- /dev/null +++ b/engine/renderapi/framebuffer/array.go @@ -0,0 +1,29 @@ +package framebuffer + +import ( + "fmt" + + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/renderpass" +) + +type Array []T + +func NewArray(count int, device device.T, name string, width, height int, pass renderpass.T) (Array, error) { + var err error + array := make(Array, count) + for i := range array { + array[i], err = New(device, fmt.Sprintf("%s[%d]", name, i), width, height, pass) + if err != nil { + return nil, err + } + } + return array, nil +} + +func (a Array) Destroy() { + for i, fbuf := range a { + fbuf.Destroy() + a[i] = nil + } +} diff --git a/engine/renderapi/framebuffer/framebuffer.go b/engine/renderapi/framebuffer/framebuffer.go new file mode 100644 index 0000000..da27858 --- /dev/null +++ b/engine/renderapi/framebuffer/framebuffer.go @@ -0,0 +1,152 @@ +package framebuffer + +import ( + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/image" + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/renderpass/attachment" + "zworld/engine/renderapi/vkerror" + "zworld/engine/util" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/core/v2/driver" +) + +type T interface { + device.Resource[core1_0.Framebuffer] + + Attachment(attachment.Name) image.View + Size() (int, int) +} + +type framebuf struct { + ptr core1_0.Framebuffer + device device.T + name string + attachments map[attachment.Name]image.View + views []image.View + images []image.T + width int + height int +} + +func New(device device.T, name string, width, height int, pass renderpass.T) (T, error) { + attachments := pass.Attachments() + depth := pass.Depth() + + images := make([]image.T, 0, len(attachments)+1) + views := make([]image.View, 0, len(attachments)+1) + attachs := make(map[attachment.Name]image.View) + + cleanup := func() { + // clean up the mess we've made so far + for _, view := range views { + view.Destroy() + } + for _, image := range images { + image.Destroy() + } + } + + allocate := func(attach attachment.T, aspect core1_0.ImageAspectFlags) error { + img, ownership, err := attach.Image().Next( + device, + name, + width, height, + ) + if err != nil { + return err + } + if ownership { + // the framebuffer is responsible for deallocating the image + images = append(images, img) + } + + view, err := img.View(img.Format(), core1_0.ImageAspectFlags(aspect)) + if err != nil { + return err + } + views = append(views, view) + + attachs[attach.Name()] = view + return nil + } + + for _, attach := range attachments { + if err := allocate(attach, core1_0.ImageAspectColor); err != nil { + cleanup() + return nil, err + } + } + if depth != nil { + if err := allocate(depth, core1_0.ImageAspectDepth); err != nil { + cleanup() + return nil, err + } + } + + info := core1_0.FramebufferCreateInfo{ + RenderPass: pass.Ptr(), + Attachments: util.Map(views, func(v image.View) core1_0.ImageView { return v.Ptr() }), + Width: width, + Height: height, + Layers: 1, + } + + var ptr core1_0.Framebuffer + ptr, result, err := device.Ptr().CreateFramebuffer(nil, info) + if err != nil { + panic(err) + } + if result != core1_0.VKSuccess { + cleanup() + return nil, vkerror.FromResult(result) + } + + device.SetDebugObjectName(driver.VulkanHandle(ptr.Handle()), core1_0.ObjectTypeFramebuffer, name) + + return &framebuf{ + ptr: ptr, + device: device, + name: name, + width: width, + height: height, + images: images, + views: views, + attachments: attachs, + }, nil +} + +func (b *framebuf) Ptr() core1_0.Framebuffer { + return b.ptr +} + +func (b *framebuf) Size() (int, int) { + return b.width, b.height +} + +func (b *framebuf) Attachment(name attachment.Name) image.View { + return b.attachments[name] +} + +func (b *framebuf) Destroy() { + if b.ptr == nil { + panic("framebuffer already destroyed") + } + + for _, image := range b.images { + image.Destroy() + } + b.images = nil + + for _, view := range b.views { + view.Destroy() + } + b.views = nil + + b.attachments = nil + + b.ptr.Destroy(nil) + b.ptr = nil + b.device = nil +} diff --git a/engine/renderapi/image/format.go b/engine/renderapi/image/format.go new file mode 100644 index 0000000..6017004 --- /dev/null +++ b/engine/renderapi/image/format.go @@ -0,0 +1,5 @@ +package image + +import "github.com/vkngwrapper/core/v2/core1_0" + +const FormatRGBA8Unorm = core1_0.FormatR8G8B8A8UnsignedNormalized diff --git a/engine/renderapi/image/image.go b/engine/renderapi/image/image.go new file mode 100644 index 0000000..d3dfaac --- /dev/null +++ b/engine/renderapi/image/image.go @@ -0,0 +1,209 @@ +package image + +import ( + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/vkerror" + "zworld/plugins/math/vec3" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/core/v2/driver" +) + +type T interface { + device.Resource[core1_0.Image] + + Key() string + Memory() device.Memory + View(format core1_0.Format, mask core1_0.ImageAspectFlags) (View, error) + Width() int + Height() int + Format() core1_0.Format + Size() vec3.T +} + +type image struct { + Args + ptr core1_0.Image + device device.T + memory device.Memory +} + +type Args struct { + Type core1_0.ImageType + Key string + Width int + Height int + Depth int + Layers int + Levels int + Format core1_0.Format + Usage core1_0.ImageUsageFlags + Tiling core1_0.ImageTiling + Sharing core1_0.SharingMode + Layout core1_0.ImageLayout + Memory core1_0.MemoryPropertyFlags +} + +func New2D(device device.T, key string, width, height int, format core1_0.Format, usage core1_0.ImageUsageFlags) (T, error) { + return New(device, Args{ + Type: core1_0.ImageType2D, + Key: key, + Width: width, + Height: height, + Depth: 1, + Layers: 1, + Levels: 1, + Format: format, + Usage: usage, + Tiling: core1_0.ImageTilingOptimal, + Sharing: core1_0.SharingModeExclusive, + Layout: core1_0.ImageLayoutUndefined, + Memory: core1_0.MemoryPropertyDeviceLocal, + }) +} + +func New(device device.T, args Args) (T, error) { + if args.Depth < 1 { + args.Depth = 1 + } + if args.Levels < 1 { + args.Levels = 1 + } + if args.Layers < 1 { + args.Layers = 1 + } + + queueIdx := device.GetQueueFamilyIndex(core1_0.QueueGraphics) + info := core1_0.ImageCreateInfo{ + ImageType: args.Type, + Format: args.Format, + Extent: core1_0.Extent3D{ + Width: args.Width, + Height: args.Height, + Depth: args.Depth, + }, + MipLevels: args.Levels, + ArrayLayers: args.Layers, + Samples: core1_0.Samples1, + Tiling: args.Tiling, + Usage: core1_0.ImageUsageFlags(args.Usage), + SharingMode: args.Sharing, + QueueFamilyIndices: []uint32{uint32(queueIdx)}, + InitialLayout: args.Layout, + } + + ptr, result, err := device.Ptr().CreateImage(nil, info) + if err != nil { + return nil, err + } + if result != core1_0.VKSuccess { + return nil, vkerror.FromResult(result) + } + + // set image debug name + if args.Key != "" { + device.SetDebugObjectName(driver.VulkanHandle(ptr.Handle()), core1_0.ObjectTypeImage, args.Key) + } + + memreq := ptr.MemoryRequirements() + + mem := device.Allocate(args.Key, core1_0.MemoryRequirements{ + Size: int(memreq.Size), + Alignment: int(memreq.Alignment), + MemoryTypeBits: memreq.MemoryTypeBits, + }, core1_0.MemoryPropertyFlags(args.Memory)) + result, err = ptr.BindImageMemory(mem.Ptr(), 0) + if err != nil { + ptr.Destroy(nil) + mem.Destroy() + return nil, err + } + if result != core1_0.VKSuccess { + ptr.Destroy(nil) + mem.Destroy() + return nil, vkerror.FromResult(result) + } + + return &image{ + Args: args, + ptr: ptr, + device: device, + memory: mem, + }, nil +} + +func Wrap(dev device.T, ptr core1_0.Image, args Args) T { + return &image{ + ptr: ptr, + device: dev, + memory: nil, + Args: args, + } +} + +func (i *image) Ptr() core1_0.Image { + return i.ptr +} + +func (i *image) Memory() device.Memory { + return i.memory +} + +func (i *image) Key() string { return i.Args.Key } +func (i *image) Width() int { return i.Args.Width } +func (i *image) Height() int { return i.Args.Height } +func (i *image) Format() core1_0.Format { return i.Args.Format } + +func (i *image) Size() vec3.T { + return vec3.T{ + X: float32(i.Args.Width), + Y: float32(i.Args.Height), + Z: float32(i.Args.Depth), + } +} + +func (i *image) Destroy() { + if i.memory != nil { + i.memory.Destroy() + if i.ptr != nil { + i.ptr.Destroy(nil) + } + } + i.ptr = nil + i.memory = nil + i.device = nil +} + +func (i *image) View(format core1_0.Format, mask core1_0.ImageAspectFlags) (View, error) { + info := core1_0.ImageViewCreateInfo{ + Image: i.ptr, + ViewType: core1_0.ImageViewType2D, + Format: format, + SubresourceRange: core1_0.ImageSubresourceRange{ + AspectMask: mask, + BaseMipLevel: 0, + LevelCount: 1, + BaseArrayLayer: 0, + LayerCount: 1, + }, + } + + ptr, result, err := i.device.Ptr().CreateImageView(nil, info) + if err != nil { + return nil, err + } + if result != core1_0.VKSuccess { + return nil, vkerror.FromResult(result) + } + + if i.Args.Key != "" { + i.device.SetDebugObjectName(driver.VulkanHandle(ptr.Handle()), core1_0.ObjectTypeImageView, i.Args.Key) + } + + return &imgview{ + ptr: ptr, + device: i.device, + image: i, + format: format, + }, nil +} diff --git a/engine/renderapi/image/loader.go b/engine/renderapi/image/loader.go new file mode 100644 index 0000000..bfeb801 --- /dev/null +++ b/engine/renderapi/image/loader.go @@ -0,0 +1,40 @@ +package image + +import ( + imglib "image" + "image/draw" + + // image codecs + _ "image/png" + + "zworld/assets" + "github.com/vkngwrapper/core/v2/core1_0" +) + +type Data struct { + Width int + Height int + Format core1_0.Format + Buffer []byte +} + +func LoadFile(file string) (*Data, error) { + imgFile, err := assets.Open(file) + if err != nil { + return nil, err + } + img, _, err := imglib.Decode(imgFile) + if err != nil { + return nil, err + } + + rgba := imglib.NewRGBA(img.Bounds()) + draw.Draw(rgba, rgba.Bounds(), img, imglib.Point{0, 0}, draw.Src) + + return &Data{ + Width: rgba.Rect.Size().X, + Height: rgba.Rect.Size().Y, + Format: FormatRGBA8Unorm, + Buffer: rgba.Pix, + }, nil +} diff --git a/engine/renderapi/image/view.go b/engine/renderapi/image/view.go new file mode 100644 index 0000000..e8df81f --- /dev/null +++ b/engine/renderapi/image/view.go @@ -0,0 +1,34 @@ +package image + +import ( + "zworld/engine/renderapi/device" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +type View interface { + device.Resource[core1_0.ImageView] + + Image() T + Format() core1_0.Format +} + +type imgview struct { + ptr core1_0.ImageView + image T + format core1_0.Format + device device.T +} + +func (v *imgview) Ptr() core1_0.ImageView { return v.ptr } +func (v *imgview) Image() T { return v.image } +func (v *imgview) Format() core1_0.Format { return v.format } + +func (v *imgview) Destroy() { + if v.ptr != nil { + v.ptr.Destroy(nil) + v.ptr = nil + } + v.device = nil + v.image = nil +} diff --git a/engine/renderapi/material/def.go b/engine/renderapi/material/def.go new file mode 100644 index 0000000..788d74f --- /dev/null +++ b/engine/renderapi/material/def.go @@ -0,0 +1,65 @@ +package material + +import ( + "strconv" + + "zworld/engine/renderapi/vertex" + + "github.com/mitchellh/hashstructure/v2" + "github.com/vkngwrapper/core/v2/core1_0" +) + +type ID uint64 + +type Pass string + +const ( + Deferred = Pass("deferred") + Forward = Pass("forward") +) + +type Def struct { + Shader string + Pass Pass + VertexFormat any + DepthTest bool + DepthWrite bool + DepthClamp bool + DepthFunc core1_0.CompareOp + Primitive vertex.Primitive + CullMode vertex.CullMode + Transparent bool + + id ID +} + +func (d *Def) Hash() ID { + if d == nil { + return 0 + } + if d.id == 0 { + // cache the hash + // todo: it might be a problem that this wont ever be invalidated + d.id = Hash(d) + } + return d.id +} + +func (d *Def) Key() string { + return strconv.FormatUint(uint64(d.Hash()), 16) +} + +func (d *Def) Version() int { + return 1 +} + +func Hash(def *Def) ID { + if def == nil { + return 0 + } + hash, err := hashstructure.Hash(*def, hashstructure.FormatV2, nil) + if err != nil { + panic(err) + } + return ID(hash) +} diff --git a/engine/renderapi/material/instance.go b/engine/renderapi/material/instance.go new file mode 100644 index 0000000..05e6ea5 --- /dev/null +++ b/engine/renderapi/material/instance.go @@ -0,0 +1,20 @@ +package material + +import ( + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/descriptor" +) + +type Instance[D descriptor.Set] struct { + material *Material[D] + set D +} + +func (i *Instance[D]) Material() *Material[D] { return i.material } +func (i *Instance[D]) Descriptors() D { return i.set } + +func (s *Instance[D]) Bind(cmd command.Buffer) { + // might want to move this to the command buffer instead to avoid the import + s.material.Bind(cmd) + cmd.CmdBindGraphicsDescriptor(s.set) +} diff --git a/engine/renderapi/material/material.go b/engine/renderapi/material/material.go new file mode 100644 index 0000000..89f7b4f --- /dev/null +++ b/engine/renderapi/material/material.go @@ -0,0 +1,128 @@ +package material + +import ( + "fmt" + "log" + + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/pipeline" + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/shader" + "zworld/engine/renderapi/texture" + "zworld/engine/renderapi/vertex" + "zworld/engine/util" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +// Materials combine pipelines and descriptors into a common unit. +type Material[D descriptor.Set] struct { + device device.T + dlayout descriptor.SetLayoutTyped[D] + shader shader.T + layout pipeline.Layout + pipe pipeline.T + pass renderpass.T +} + +type Args struct { + Shader shader.T + Pass renderpass.T + Subpass renderpass.Name + Constants []pipeline.PushConstant + + Pointers vertex.Pointers + Primitive vertex.Primitive + DepthTest bool + DepthWrite bool + DepthClamp bool + DepthBias float32 + DepthSlope float32 + DepthFunc core1_0.CompareOp + CullMode vertex.CullMode +} + +func New[D descriptor.Set](device device.T, args Args, descriptors D) *Material[D] { + if device == nil { + panic("device is nil") + } + if args.Shader == nil { + panic("shader is nil") + } + + for i, ptr := range args.Pointers { + if index, kind, exists := args.Shader.Input(ptr.Name); exists { + ptr.Bind(index, kind) + args.Pointers[i] = ptr + } else { + log.Printf("no attribute in shader %s\n", ptr.Name) + } + } + + if args.Primitive == 0 { + args.Primitive = vertex.Triangles + } + + // create new descriptor set layout + // ... this could be cached ... + descLayout := descriptor.New(device, descriptors, args.Shader) + + // crete pipeline layout + // ... this could be cached ... + layout := pipeline.NewLayout(device, []descriptor.SetLayout{descLayout}, args.Constants) + + pipelineName := fmt.Sprintf("%s/%s", args.Pass.Name(), args.Shader.Name()) + pipe := pipeline.New(device, pipeline.Args{ + Key: pipelineName, + Layout: layout, + Pass: args.Pass, + Subpass: args.Subpass, + Shader: args.Shader, + Pointers: args.Pointers, + + Primitive: args.Primitive, + DepthTest: args.DepthTest, + DepthWrite: args.DepthWrite, + DepthClamp: args.DepthClamp, + DepthFunc: args.DepthFunc, + CullMode: args.CullMode, + }) + + return &Material[D]{ + device: device, + shader: args.Shader, + + dlayout: descLayout, + layout: layout, + pipe: pipe, + pass: args.Pass, + } +} + +func (m *Material[D]) Bind(cmd command.Buffer) { + cmd.CmdBindGraphicsPipeline(m.pipe) +} + +func (m *Material[D]) TextureSlots() []texture.Slot { + return m.shader.Textures() +} + +func (m *Material[D]) Destroy() { + m.dlayout.Destroy() + m.pipe.Destroy() + m.layout.Destroy() +} + +func (m *Material[D]) Instantiate(pool descriptor.Pool) *Instance[D] { + set := m.dlayout.Instantiate(pool) + return &Instance[D]{ + material: m, + set: set, + } +} + +func (m *Material[D]) InstantiateMany(pool descriptor.Pool, n int) []*Instance[D] { + return util.Map(util.Range(0, n, 1), func(i int) *Instance[D] { return m.Instantiate(pool) }) +} diff --git a/engine/renderapi/material/types.go b/engine/renderapi/material/types.go new file mode 100644 index 0000000..356784d --- /dev/null +++ b/engine/renderapi/material/types.go @@ -0,0 +1,76 @@ +package material + +import ( + "zworld/engine/renderapi/vertex" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +// todo: this is rather implementation specific and likely +// does not belong in the renderapi package + +func StandardDeferred() *Def { + return &Def{ + Pass: Deferred, + Shader: "deferred/textured", + VertexFormat: vertex.T{}, + DepthTest: true, + DepthWrite: true, + Primitive: vertex.Triangles, + CullMode: vertex.CullBack, + } +} + +func StandardForward() *Def { + return &Def{ + Pass: Forward, + Shader: "forward/textured", + VertexFormat: vertex.T{}, + DepthTest: true, + DepthWrite: true, + DepthFunc: core1_0.CompareOpLessOrEqual, + Primitive: vertex.Triangles, + CullMode: vertex.CullBack, + Transparent: false, + } +} + +func TransparentForward() *Def { + return &Def{ + Pass: Forward, + Shader: "forward/textured", + VertexFormat: vertex.T{}, + DepthTest: true, + DepthWrite: true, + DepthFunc: core1_0.CompareOpLessOrEqual, + Primitive: vertex.Triangles, + CullMode: vertex.CullBack, + Transparent: true, + } +} + +func ColoredForward() *Def { + return &Def{ + Pass: Forward, + Shader: "forward/color", + VertexFormat: vertex.C{}, + DepthTest: true, + DepthWrite: true, + DepthFunc: core1_0.CompareOpLessOrEqual, + Primitive: vertex.Triangles, + CullMode: vertex.CullBack, + } +} + +func Lines() *Def { + return &Def{ + Shader: "lines", + Pass: "lines", + VertexFormat: vertex.C{}, + Primitive: vertex.Lines, + DepthTest: true, + DepthWrite: false, + DepthFunc: core1_0.CompareOpLessOrEqual, + CullMode: vertex.CullNone, + } +} diff --git a/engine/renderapi/noise/white_noise.go b/engine/renderapi/noise/white_noise.go new file mode 100644 index 0000000..b42c592 --- /dev/null +++ b/engine/renderapi/noise/white_noise.go @@ -0,0 +1,48 @@ +package noise + +import ( + "fmt" + "math/rand" + + "zworld/engine/renderapi/image" + "zworld/engine/renderapi/texture" +) + +type WhiteNoise struct { + Width int + Height int + + key string +} + +func NewWhiteNoise(width, height int) *WhiteNoise { + return &WhiteNoise{ + key: fmt.Sprintf("noise-white-%dx%d", width, height), + Width: width, + Height: height, + } +} + +func (n *WhiteNoise) Key() string { return n.key } +func (n *WhiteNoise) Version() int { return 1 } + +func (n *WhiteNoise) ImageData() *image.Data { + buffer := make([]byte, 4*n.Width*n.Height) + _, err := rand.Read(buffer) + if err != nil { + panic(err) + } + return &image.Data{ + Width: n.Width, + Height: n.Height, + Format: image.FormatRGBA8Unorm, + Buffer: buffer, + } +} + +func (n *WhiteNoise) TextureArgs() texture.Args { + return texture.Args{ + Filter: texture.FilterLinear, + Wrap: texture.WrapRepeat, + } +} diff --git a/engine/renderapi/pipeline/args.go b/engine/renderapi/pipeline/args.go new file mode 100644 index 0000000..0cc6822 --- /dev/null +++ b/engine/renderapi/pipeline/args.go @@ -0,0 +1,50 @@ +package pipeline + +import ( + "reflect" + + "zworld/engine/renderapi/renderpass" + "zworld/engine/renderapi/shader" + "zworld/engine/renderapi/vertex" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +type Args struct { + Key string + Pass renderpass.T + Subpass renderpass.Name + Layout Layout + Shader shader.T + Pointers vertex.Pointers + + Primitive vertex.Primitive + PolygonFillMode core1_0.PolygonMode + CullMode vertex.CullMode + + DepthTest bool + DepthWrite bool + DepthClamp bool + DepthFunc core1_0.CompareOp + + StencilTest bool +} + +func (args *Args) defaults() { + if args.DepthFunc == 0 { + args.DepthFunc = core1_0.CompareOpLessOrEqual + } + if args.Primitive == 0 { + args.Primitive = vertex.Triangles + } +} + +type PushConstant struct { + Stages core1_0.ShaderStageFlags + Type any +} + +func (p *PushConstant) Size() int { + t := reflect.TypeOf(p.Type) + return int(t.Size()) +} diff --git a/engine/renderapi/pipeline/layout.go b/engine/renderapi/pipeline/layout.go new file mode 100644 index 0000000..12be5e3 --- /dev/null +++ b/engine/renderapi/pipeline/layout.go @@ -0,0 +1,63 @@ +package pipeline + +import ( + "log" + + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/device" + "zworld/engine/util" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +type Layout interface { + device.Resource[core1_0.PipelineLayout] +} + +type layout struct { + ptr core1_0.PipelineLayout + device device.T +} + +func NewLayout(device device.T, descriptors []descriptor.SetLayout, constants []PushConstant) Layout { + offset := 0 + info := core1_0.PipelineLayoutCreateInfo{ + + SetLayouts: util.Map(descriptors, func(desc descriptor.SetLayout) core1_0.DescriptorSetLayout { + return desc.Ptr() + }), + + PushConstantRanges: util.Map(constants, func(push PushConstant) core1_0.PushConstantRange { + size := push.Size() + log.Printf("push: %d bytes", size) + rng := core1_0.PushConstantRange{ + StageFlags: core1_0.ShaderStageFlags(push.Stages), + Offset: offset, + Size: size, + } + offset += size + return rng + }), + } + + ptr, _, err := device.Ptr().CreatePipelineLayout(nil, info) + if err != nil { + panic(err) + } + + return &layout{ + ptr: ptr, + device: device, + } +} + +func (l *layout) Ptr() core1_0.PipelineLayout { + return l.ptr +} + +func (l *layout) Destroy() { + if l.ptr != nil { + l.ptr.Destroy(nil) + l.ptr = nil + } +} diff --git a/engine/renderapi/pipeline/pipeline.go b/engine/renderapi/pipeline/pipeline.go new file mode 100644 index 0000000..fb3a490 --- /dev/null +++ b/engine/renderapi/pipeline/pipeline.go @@ -0,0 +1,299 @@ +package pipeline + +import ( + "fmt" + "log" + + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/renderpass/attachment" + "zworld/engine/renderapi/shader" + "zworld/engine/renderapi/types" + "zworld/engine/renderapi/vertex" + "zworld/engine/util" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/core/v2/driver" +) + +type T interface { + device.Resource[core1_0.Pipeline] + + Layout() Layout +} + +type pipeline struct { + ptr core1_0.Pipeline + device device.T + layout Layout + args Args +} + +func New(device device.T, args Args) T { + args.defaults() + log.Println("creating pipeline", args.Key) + + // todo: pipeline cache + // could probably be controlled a global setting? + + modules := util.Map(args.Shader.Modules(), func(shader shader.Module) core1_0.PipelineShaderStageCreateInfo { + return core1_0.PipelineShaderStageCreateInfo{ + Module: shader.Ptr(), + Name: shader.Entrypoint(), + Stage: core1_0.ShaderStageFlags(shader.Stage()), + } + }) + + log.Println(" depth test:", args.DepthTest) + log.Println(" depth func:", args.DepthFunc) + log.Println(" depth write:", args.DepthWrite) + + log.Println(" attributes", args.Pointers) + attrs := pointersToVertexAttributes(args.Pointers, 0) + + subpass := args.Pass.Subpass(args.Subpass) + log.Println(" subpass:", subpass.Name, subpass.Index()) + + blendStates := util.Map(subpass.ColorAttachments, func(name attachment.Name) core1_0.PipelineColorBlendAttachmentState { + attach := args.Pass.Attachment(name) + // todo: move into attachment object + // or into the material/pipeline object? + blend := attach.Blend() + return core1_0.PipelineColorBlendAttachmentState{ + // additive blending + BlendEnabled: blend.Enabled, + ColorBlendOp: blend.Color.Operation, + SrcColorBlendFactor: blend.Color.SrcFactor, + DstColorBlendFactor: blend.Color.DstFactor, + AlphaBlendOp: blend.Alpha.Operation, + SrcAlphaBlendFactor: blend.Alpha.SrcFactor, + DstAlphaBlendFactor: blend.Alpha.DstFactor, + ColorWriteMask: core1_0.ColorComponentRed | core1_0.ColorComponentGreen | + core1_0.ColorComponentBlue | core1_0.ColorComponentAlpha, + } + }) + + info := core1_0.GraphicsPipelineCreateInfo{ + // layout + Layout: args.Layout.Ptr(), + Subpass: subpass.Index(), + + // renderapi pass + RenderPass: args.Pass.Ptr(), + + // Stages + Stages: modules, + + // Vertex input state + VertexInputState: &core1_0.PipelineVertexInputStateCreateInfo{ + VertexBindingDescriptions: []core1_0.VertexInputBindingDescription{ + { + Binding: 0, + Stride: args.Pointers.Stride(), + InputRate: core1_0.VertexInputRateVertex, + }, + }, + VertexAttributeDescriptions: attrs, + }, + + // Input assembly + InputAssemblyState: &core1_0.PipelineInputAssemblyStateCreateInfo{ + Topology: core1_0.PrimitiveTopology(args.Primitive), + }, + + // viewport state + // does not seem to matter so much since we set it dynamically every frame + ViewportState: &core1_0.PipelineViewportStateCreateInfo{ + Viewports: []core1_0.Viewport{ + { + Width: 1000, + Height: 1000, + MinDepth: 0, + MaxDepth: 1, + }, + }, + Scissors: []core1_0.Rect2D{ + // scissor + { + Offset: core1_0.Offset2D{}, + Extent: core1_0.Extent2D{ + Width: 1000, + Height: 1000, + }, + }, + }, + }, + + // rasterization state + RasterizationState: &core1_0.PipelineRasterizationStateCreateInfo{ + DepthClampEnable: args.DepthClamp, + DepthBiasEnable: false, + RasterizerDiscardEnable: false, + PolygonMode: args.PolygonFillMode, + CullMode: core1_0.CullModeFlags(args.CullMode), + LineWidth: 1, + + // clockwise in vulkans right-handed coordinates is equivalent to the + // traditional opengl counter-clockwise winding, which is in line with + // the left-handed world space coordinate system. + FrontFace: core1_0.FrontFaceClockwise, + }, + + // multisample + MultisampleState: &core1_0.PipelineMultisampleStateCreateInfo{ + RasterizationSamples: core1_0.Samples1, + }, + + // depth & stencil + DepthStencilState: &core1_0.PipelineDepthStencilStateCreateInfo{ + // enable depth testing with less or + DepthTestEnable: args.DepthTest, + DepthWriteEnable: args.DepthWrite, + DepthCompareOp: args.DepthFunc, + DepthBoundsTestEnable: false, + Back: core1_0.StencilOpState{ + FailOp: core1_0.StencilKeep, + PassOp: core1_0.StencilKeep, + CompareOp: core1_0.CompareOpAlways, + }, + StencilTestEnable: args.StencilTest, + Front: core1_0.StencilOpState{ + FailOp: core1_0.StencilKeep, + PassOp: core1_0.StencilKeep, + CompareOp: core1_0.CompareOpAlways, + }, + }, + + // color blending + ColorBlendState: &core1_0.PipelineColorBlendStateCreateInfo{ + LogicOpEnabled: false, + LogicOp: core1_0.LogicOpClear, + Attachments: blendStates, + }, + + // dynamic state: viewport & scissor + DynamicState: &core1_0.PipelineDynamicStateCreateInfo{ + DynamicStates: []core1_0.DynamicState{ + core1_0.DynamicStateViewport, + core1_0.DynamicStateScissor, + }, + }, + } + + ptrs, result, err := device.Ptr().CreateGraphicsPipelines(nil, nil, []core1_0.GraphicsPipelineCreateInfo{info}) + if err != nil { + panic(err) + } + if result != core1_0.VKSuccess { + panic("failed to create pipeline") + } + + if args.Key != "" { + device.SetDebugObjectName(driver.VulkanHandle(ptrs[0].Handle()), core1_0.ObjectTypePipeline, args.Key) + } + + return &pipeline{ + ptr: ptrs[0], + device: device, + layout: args.Layout, + args: args, + } +} + +func (p *pipeline) Ptr() core1_0.Pipeline { + return p.ptr +} + +func (p *pipeline) Layout() Layout { + return p.layout +} + +func (p *pipeline) Destroy() { + p.ptr.Destroy(nil) + p.ptr = nil +} + +func pointersToVertexAttributes(ptrs vertex.Pointers, binding int) []core1_0.VertexInputAttributeDescription { + attrs := make([]core1_0.VertexInputAttributeDescription, 0, len(ptrs)) + for _, ptr := range ptrs { + if ptr.Binding < 0 { + continue + } + attrs = append(attrs, core1_0.VertexInputAttributeDescription{ + Binding: binding, + Location: uint32(ptr.Binding), + Format: convertFormat(ptr), + Offset: ptr.Offset, + }) + } + return attrs +} + +type ptrType struct { + Source types.Type + Target types.Type + Elements int + Normalize bool +} + +var formatMap = map[ptrType]core1_0.Format{ + {types.Float, types.Float, 1, false}: core1_0.FormatR32SignedFloat, + {types.Float, types.Float, 2, false}: core1_0.FormatR32G32SignedFloat, + {types.Float, types.Float, 3, false}: core1_0.FormatR32G32B32SignedFloat, + {types.Float, types.Float, 4, false}: core1_0.FormatR32G32B32A32SignedFloat, + {types.Int8, types.Int8, 1, false}: core1_0.FormatR8SignedInt, + {types.Int8, types.Int8, 2, false}: core1_0.FormatR8G8SignedInt, + {types.Int8, types.Int8, 3, false}: core1_0.FormatR8G8B8SignedInt, + {types.Int8, types.Int8, 4, false}: core1_0.FormatR8G8B8A8SignedInt, + {types.Int8, types.Float, 4, true}: core1_0.FormatR8SignedNormalized, + {types.Int8, types.Float, 2, true}: core1_0.FormatR8G8SignedNormalized, + {types.Int8, types.Float, 3, true}: core1_0.FormatR8G8B8SignedNormalized, + {types.Int8, types.Float, 4, true}: core1_0.FormatR8G8B8A8SignedNormalized, + {types.UInt8, types.UInt8, 1, false}: core1_0.FormatR8UnsignedInt, + {types.UInt8, types.UInt8, 2, false}: core1_0.FormatR8G8UnsignedInt, + {types.UInt8, types.UInt8, 3, false}: core1_0.FormatR8G8B8UnsignedInt, + {types.UInt8, types.UInt8, 4, false}: core1_0.FormatR8G8B8A8UnsignedInt, + {types.UInt8, types.Float, 1, false}: core1_0.FormatR8UnsignedScaled, + {types.UInt8, types.Float, 2, false}: core1_0.FormatR8G8UnsignedScaled, + {types.UInt8, types.Float, 3, false}: core1_0.FormatR8G8B8UnsignedScaled, + {types.UInt8, types.Float, 4, false}: core1_0.FormatR8G8B8A8UnsignedScaled, + {types.UInt8, types.Float, 1, true}: core1_0.FormatR8UnsignedNormalized, + {types.UInt8, types.Float, 2, true}: core1_0.FormatR8G8UnsignedNormalized, + {types.UInt8, types.Float, 3, true}: core1_0.FormatR8G8B8UnsignedNormalized, + {types.UInt8, types.Float, 4, true}: core1_0.FormatR8G8B8A8UnsignedNormalized, + {types.Int16, types.Int16, 1, false}: core1_0.FormatR16SignedInt, + {types.Int16, types.Int16, 2, false}: core1_0.FormatR16G16SignedInt, + {types.Int16, types.Int16, 3, false}: core1_0.FormatR16G16B16SignedInt, + {types.Int16, types.Int16, 4, false}: core1_0.FormatR16G16B16A16SignedInt, + {types.Int16, types.Float, 4, true}: core1_0.FormatR16SignedNormalized, + {types.Int16, types.Float, 2, true}: core1_0.FormatR16G16SignedNormalized, + {types.Int16, types.Float, 3, true}: core1_0.FormatR16G16B16SignedNormalized, + {types.Int16, types.Float, 4, true}: core1_0.FormatR16G16B16A16SignedNormalized, + {types.UInt16, types.UInt16, 1, false}: core1_0.FormatR16UnsignedInt, + {types.UInt16, types.UInt16, 2, false}: core1_0.FormatR16G16UnsignedInt, + {types.UInt16, types.UInt16, 3, false}: core1_0.FormatR16G16B16UnsignedInt, + {types.UInt16, types.UInt16, 4, false}: core1_0.FormatR16G16B16A16UnsignedInt, + {types.UInt16, types.Float, 1, true}: core1_0.FormatR16UnsignedNormalized, + {types.UInt16, types.Float, 2, true}: core1_0.FormatR16G16UnsignedNormalized, + {types.UInt16, types.Float, 3, true}: core1_0.FormatR16G16B16UnsignedNormalized, + {types.UInt16, types.Float, 4, true}: core1_0.FormatR16G16B16A16UnsignedNormalized, + {types.UInt16, types.Float, 1, false}: core1_0.FormatR16UnsignedScaled, + {types.UInt16, types.Float, 2, false}: core1_0.FormatR16G16UnsignedScaled, + {types.UInt16, types.Float, 3, false}: core1_0.FormatR16G16B16UnsignedScaled, + {types.UInt16, types.Float, 4, false}: core1_0.FormatR16G16B16A16UnsignedScaled, + {types.Int32, types.Int32, 1, false}: core1_0.FormatR32SignedInt, + {types.Int32, types.Int32, 2, false}: core1_0.FormatR32G32SignedInt, + {types.Int32, types.Int32, 3, false}: core1_0.FormatR32G32B32SignedInt, + {types.Int32, types.Int32, 4, false}: core1_0.FormatR32G32B32A32SignedInt, + {types.UInt32, types.UInt32, 1, false}: core1_0.FormatR32UnsignedInt, + {types.UInt32, types.UInt32, 2, false}: core1_0.FormatR32G32UnsignedInt, + {types.UInt32, types.UInt32, 3, false}: core1_0.FormatR32G32B32UnsignedInt, + {types.UInt32, types.UInt32, 4, false}: core1_0.FormatR32G32B32A32UnsignedInt, +} + +func convertFormat(ptr vertex.Pointer) core1_0.Format { + kind := ptrType{ptr.Source, ptr.Destination, ptr.Elements, ptr.Normalize} + if fmt, exists := formatMap[kind]; exists { + return fmt + } + panic(fmt.Sprintf("illegal format in pointer %s from %s -> %s x%d (normalize: %t)", ptr.Name, ptr.Source, ptr.Destination, ptr.Elements, ptr.Normalize)) +} diff --git a/engine/renderapi/renderpass/args.go b/engine/renderapi/renderpass/args.go new file mode 100644 index 0000000..0a9d582 --- /dev/null +++ b/engine/renderapi/renderpass/args.go @@ -0,0 +1,14 @@ +package renderpass + +import ( + "zworld/engine/renderapi/renderpass/attachment" +) + +type Args struct { + Name string + ColorAttachments []attachment.Color + DepthAttachment *attachment.Depth + + Subpasses []Subpass + Dependencies []SubpassDependency +} diff --git a/engine/renderapi/renderpass/attachment/attachment.go b/engine/renderapi/renderpass/attachment/attachment.go new file mode 100644 index 0000000..52c9c5c --- /dev/null +++ b/engine/renderapi/renderpass/attachment/attachment.go @@ -0,0 +1,44 @@ +package attachment + +import ( + "github.com/vkngwrapper/core/v2/core1_0" +) + +type Name string + +type T interface { + Name() Name + Image() Image + Clear() core1_0.ClearValue + Description() core1_0.AttachmentDescription + Blend() Blend +} + +type BlendOp struct { + Operation core1_0.BlendOp + SrcFactor core1_0.BlendFactor + DstFactor core1_0.BlendFactor +} + +type Blend struct { + Enabled bool + Color BlendOp + Alpha BlendOp +} + +type attachment struct { + name Name + image Image + clear core1_0.ClearValue + desc core1_0.AttachmentDescription + blend Blend +} + +func (a *attachment) Description() core1_0.AttachmentDescription { + return a.desc +} + +func (a *attachment) Name() Name { return a.name } +func (a *attachment) Image() Image { return a.image } +func (a *attachment) Clear() core1_0.ClearValue { return a.clear } +func (a *attachment) Blend() Blend { return a.blend } diff --git a/engine/renderapi/renderpass/attachment/blend.go b/engine/renderapi/renderpass/attachment/blend.go new file mode 100644 index 0000000..f42333b --- /dev/null +++ b/engine/renderapi/renderpass/attachment/blend.go @@ -0,0 +1,47 @@ +package attachment + +import ( + "github.com/vkngwrapper/core/v2/core1_0" +) + +var BlendMix = Blend{ + Enabled: true, + Color: BlendOp{ + Operation: core1_0.BlendOpAdd, + SrcFactor: core1_0.BlendFactorSrcAlpha, + DstFactor: core1_0.BlendFactorOneMinusSrcAlpha, + }, + Alpha: BlendOp{ + Operation: core1_0.BlendOpAdd, + SrcFactor: core1_0.BlendFactorOne, + DstFactor: core1_0.BlendFactorZero, + }, +} + +var BlendAdditive = Blend{ + Enabled: true, + Color: BlendOp{ + Operation: core1_0.BlendOpAdd, + SrcFactor: core1_0.BlendFactorOne, + DstFactor: core1_0.BlendFactorOne, + }, + Alpha: BlendOp{ + Operation: core1_0.BlendOpAdd, + SrcFactor: core1_0.BlendFactorOne, + DstFactor: core1_0.BlendFactorZero, + }, +} + +var BlendMultiply = Blend{ + Enabled: true, + Color: BlendOp{ + Operation: core1_0.BlendOpAdd, + SrcFactor: core1_0.BlendFactorSrcAlpha, + DstFactor: core1_0.BlendFactorOneMinusSrcAlpha, + }, + Alpha: BlendOp{ + Operation: core1_0.BlendOpAdd, + SrcFactor: core1_0.BlendFactorSrcAlpha, + DstFactor: core1_0.BlendFactorOneMinusSrcAlpha, + }, +} diff --git a/engine/renderapi/renderpass/attachment/color_attachment.go b/engine/renderapi/renderpass/attachment/color_attachment.go new file mode 100644 index 0000000..56f56aa --- /dev/null +++ b/engine/renderapi/renderpass/attachment/color_attachment.go @@ -0,0 +1,54 @@ +package attachment + +import ( + "zworld/engine/renderapi/color" + "zworld/engine/renderapi/device" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +type Color struct { + Name Name + Samples core1_0.SampleCountFlags + LoadOp core1_0.AttachmentLoadOp + StoreOp core1_0.AttachmentStoreOp + InitialLayout core1_0.ImageLayout + FinalLayout core1_0.ImageLayout + Clear color.T + Image Image + Blend Blend +} + +func (desc *Color) defaults() { + if desc.Samples == 0 { + desc.Samples = core1_0.Samples1 + } + if desc.Image == nil { + panic("no image reference") + } +} + +func NewColor(device device.T, desc Color) T { + desc.defaults() + + clear := core1_0.ClearValueFloat{desc.Clear.R, desc.Clear.G, desc.Clear.B, desc.Clear.A} + + return &attachment{ + name: desc.Name, + image: desc.Image, + clear: clear, + blend: desc.Blend, + desc: core1_0.AttachmentDescription{ + Format: desc.Image.Format(), + Samples: desc.Samples, + LoadOp: desc.LoadOp, + StoreOp: desc.StoreOp, + InitialLayout: desc.InitialLayout, + FinalLayout: desc.FinalLayout, + + // color attachments dont have stencil buffers, so we dont care about them + StencilLoadOp: core1_0.AttachmentLoadOpDontCare, + StencilStoreOp: core1_0.AttachmentStoreOpDontCare, + }, + } +} diff --git a/engine/renderapi/renderpass/attachment/depth_attachment.go b/engine/renderapi/renderpass/attachment/depth_attachment.go new file mode 100644 index 0000000..ba4822c --- /dev/null +++ b/engine/renderapi/renderpass/attachment/depth_attachment.go @@ -0,0 +1,58 @@ +package attachment + +import ( + "zworld/engine/renderapi/device" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +const DepthName Name = "depth" + +type Depth struct { + Samples core1_0.SampleCountFlags + LoadOp core1_0.AttachmentLoadOp + StoreOp core1_0.AttachmentStoreOp + StencilLoadOp core1_0.AttachmentLoadOp + StencilStoreOp core1_0.AttachmentStoreOp + InitialLayout core1_0.ImageLayout + FinalLayout core1_0.ImageLayout + ClearDepth float32 + ClearStencil uint32 + + // Allocation strategy. Defaults to allocating new images. + Image Image +} + +func (desc *Depth) defaults() { + if desc.Samples == 0 { + desc.Samples = core1_0.Samples1 + } + if desc.Image == nil { + panic("no image reference") + } +} + +func NewDepth(device device.T, desc Depth) T { + desc.defaults() + + clear := core1_0.ClearValueDepthStencil{ + Depth: desc.ClearDepth, + Stencil: desc.ClearStencil, + } + + return &attachment{ + name: DepthName, + image: desc.Image, + clear: clear, + desc: core1_0.AttachmentDescription{ + Format: desc.Image.Format(), + Samples: desc.Samples, + LoadOp: desc.LoadOp, + StoreOp: desc.StoreOp, + StencilLoadOp: desc.StencilLoadOp, + StencilStoreOp: desc.StencilStoreOp, + InitialLayout: desc.InitialLayout, + FinalLayout: desc.FinalLayout, + }, + } +} diff --git a/engine/renderapi/renderpass/attachment/image.go b/engine/renderapi/renderpass/attachment/image.go new file mode 100644 index 0000000..747b16e --- /dev/null +++ b/engine/renderapi/renderpass/attachment/image.go @@ -0,0 +1,105 @@ +package attachment + +import ( + "errors" + "fmt" + "log" + + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/image" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +var ErrArrayExhausted = errors.New("image array allocator exhausted") + +type Image interface { + Format() core1_0.Format + Next(device device.T, name string, width, height int) (image.T, bool, error) +} + +type alloc struct { + key string + format core1_0.Format + usage core1_0.ImageUsageFlags +} + +var _ Image = &alloc{} + +func (im *alloc) Format() core1_0.Format { + return im.format +} + +func (im *alloc) Next( + device device.T, + name string, + width, height int, +) (image.T, bool, error) { + key := fmt.Sprintf("%s-%s", name, im.key) + log.Println("attachment alloc", key) + img, err := image.New2D( + device, + key, + width, height, + im.format, im.usage, + ) + return img, true, err +} + +func NewImage(key string, format core1_0.Format, usage core1_0.ImageUsageFlags) Image { + return &alloc{ + key: key, + format: format, + usage: usage, + } +} + +type imageArray struct { + images []image.T + next int +} + +func (im *imageArray) Format() core1_0.Format { + return im.images[0].Format() +} + +func (im *imageArray) Next( + device device.T, + name string, + width, height int, +) (image.T, bool, error) { + if im.next >= len(im.images) { + return nil, false, ErrArrayExhausted + } + img := im.images[im.next] + im.next++ + return img, false, nil +} + +func FromImageArray(images []image.T) Image { + return &imageArray{ + images: images, + next: 0, + } +} + +// FromImage returns an allocator that always returns a reference to the provided image. +func FromImage(img image.T) Image { + return &imageRef{image: img} +} + +type imageRef struct { + image image.T +} + +func (im *imageRef) Format() core1_0.Format { + return im.image.Format() +} + +func (im *imageRef) Next( + device device.T, + name string, + width, height int, +) (image.T, bool, error) { + return im.image, false, nil +} diff --git a/engine/renderapi/renderpass/renderpass.go b/engine/renderapi/renderpass/renderpass.go new file mode 100644 index 0000000..6f63a41 --- /dev/null +++ b/engine/renderapi/renderpass/renderpass.go @@ -0,0 +1,200 @@ +package renderpass + +import ( + "fmt" + "log" + + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/renderpass/attachment" + "zworld/engine/util" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/core/v2/driver" +) + +type T interface { + device.Resource[core1_0.RenderPass] + + Depth() attachment.T + Attachment(name attachment.Name) attachment.T + Attachments() []attachment.T + Subpass(name Name) Subpass + Clear() []core1_0.ClearValue + Name() string +} + +type renderpass struct { + device device.T + ptr core1_0.RenderPass + subpasses []Subpass + passIndices map[Name]int + attachments []attachment.T + depth attachment.T + indices map[attachment.Name]int + clear []core1_0.ClearValue + name string +} + +func New(device device.T, args Args) T { + clear := make([]core1_0.ClearValue, 0, len(args.ColorAttachments)+1) + attachments := make([]attachment.T, len(args.ColorAttachments)) + attachmentIndices := make(map[attachment.Name]int) + + log.Println("create renderpass", args.Name) + log.Println("attachments") + for index, desc := range args.ColorAttachments { + attachment := attachment.NewColor(device, desc) + clear = append(clear, attachment.Clear()) + attachments[index] = attachment + attachmentIndices[attachment.Name()] = index + log.Printf(" %d: %s", index, desc.Name) + } + + var depth attachment.T + if args.DepthAttachment != nil { + index := len(attachments) + attachmentIndices[attachment.DepthName] = index + depth = attachment.NewDepth(device, *args.DepthAttachment) + clear = append(clear, depth.Clear()) + log.Printf(" %d: %s", index, attachment.DepthName) + } + + descriptions := make([]core1_0.AttachmentDescription, 0, len(args.ColorAttachments)+1) + for _, attachment := range attachments { + descriptions = append(descriptions, attachment.Description()) + } + if depth != nil { + descriptions = append(descriptions, depth.Description()) + } + + subpasses := make([]core1_0.SubpassDescription, 0, len(args.Subpasses)) + subpassIndices := make(map[Name]int) + + for idx, subpass := range args.Subpasses { + log.Println("subpass", idx) + + var depthRef *core1_0.AttachmentReference + if depth != nil && subpass.Depth { + idx := attachmentIndices[attachment.DepthName] + depthRef = &core1_0.AttachmentReference{ + Attachment: idx, + Layout: core1_0.ImageLayoutDepthStencilAttachmentOptimal, + } + log.Printf(" depth -> %s (%d)\n", attachment.DepthName, idx) + } + + subpasses = append(subpasses, core1_0.SubpassDescription{ + PipelineBindPoint: core1_0.PipelineBindPointGraphics, + + ColorAttachments: util.MapIdx( + subpass.ColorAttachments, + func(name attachment.Name, i int) core1_0.AttachmentReference { + idx := attachmentIndices[name] + log.Printf(" color %d -> %s (%d)\n", i, name, idx) + return core1_0.AttachmentReference{ + Attachment: idx, + Layout: core1_0.ImageLayoutColorAttachmentOptimal, + } + }), + + InputAttachments: util.MapIdx( + subpass.InputAttachments, + func(name attachment.Name, i int) core1_0.AttachmentReference { + idx := attachmentIndices[name] + log.Printf(" input %d -> %s (%d)\n", i, name, idx) + return core1_0.AttachmentReference{ + Attachment: idx, + Layout: core1_0.ImageLayoutShaderReadOnlyOptimal, + } + }), + + DepthStencilAttachment: depthRef, + }) + + subpassIndices[subpass.Name] = idx + args.Subpasses[idx].index = idx + } + + dependencies := make([]core1_0.SubpassDependency, len(args.Dependencies)) + for idx, dependency := range args.Dependencies { + src := core1_0.SubpassExternal + if dependency.Src != ExternalSubpass { + src = subpassIndices[dependency.Src] + } + dst := core1_0.SubpassExternal + if dependency.Dst != ExternalSubpass { + dst = subpassIndices[dependency.Dst] + } + dependencies[idx] = core1_0.SubpassDependency{ + SrcSubpass: src, + DstSubpass: dst, + SrcStageMask: core1_0.PipelineStageFlags(dependency.SrcStageMask), + SrcAccessMask: core1_0.AccessFlags(dependency.SrcAccessMask), + DstStageMask: core1_0.PipelineStageFlags(dependency.DstStageMask), + DstAccessMask: core1_0.AccessFlags(dependency.DstAccessMask), + DependencyFlags: core1_0.DependencyFlags(dependency.Flags), + } + } + + ptr, _, err := device.Ptr().CreateRenderPass(nil, core1_0.RenderPassCreateInfo{ + Attachments: descriptions, + Subpasses: subpasses, + SubpassDependencies: dependencies, + }) + if err != nil { + panic(err) + } + + // set object name + device.SetDebugObjectName(driver.VulkanHandle(ptr.Handle()), core1_0.ObjectTypeRenderPass, args.Name) + + return &renderpass{ + device: device, + ptr: ptr, + depth: depth, + indices: attachmentIndices, + attachments: attachments, + passIndices: subpassIndices, + subpasses: args.Subpasses, + clear: clear, + name: args.Name, + } +} + +func (r *renderpass) Ptr() core1_0.RenderPass { return r.ptr } +func (r *renderpass) Depth() attachment.T { return r.depth } +func (r *renderpass) Name() string { return r.name } + +func (r *renderpass) Attachment(name attachment.Name) attachment.T { + if name == attachment.DepthName { + return r.depth + } + index := r.indices[name] + return r.attachments[index] +} + +func (r *renderpass) Clear() []core1_0.ClearValue { + return r.clear +} + +func (r *renderpass) Attachments() []attachment.T { + return r.attachments +} + +func (r *renderpass) Subpass(name Name) Subpass { + if name == "" { + return r.subpasses[0] + } + idx, exists := r.passIndices[name] + if !exists { + panic(fmt.Sprintf("unknown subpass %s", name)) + } + return r.subpasses[idx] +} + +func (r *renderpass) Destroy() { + if r.ptr != nil { + r.ptr.Destroy(nil) + r.ptr = nil + } +} diff --git a/engine/renderapi/renderpass/subpass.go b/engine/renderapi/renderpass/subpass.go new file mode 100644 index 0000000..ba67978 --- /dev/null +++ b/engine/renderapi/renderpass/subpass.go @@ -0,0 +1,35 @@ +package renderpass + +import ( + "zworld/engine/renderapi/renderpass/attachment" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +type Name string + +const ExternalSubpass Name = "external" + +type Subpass struct { + index int + + Name Name + Depth bool + ColorAttachments []attachment.Name + InputAttachments []attachment.Name +} + +func (s *Subpass) Index() int { + return s.index +} + +type SubpassDependency struct { + Src Name + Dst Name + + Flags core1_0.DependencyFlags + SrcStageMask core1_0.PipelineStageFlags + SrcAccessMask core1_0.AccessFlags + DstStageMask core1_0.PipelineStageFlags + DstAccessMask core1_0.AccessFlags +} diff --git a/engine/renderapi/shader/details.go b/engine/renderapi/shader/details.go new file mode 100644 index 0000000..89504f0 --- /dev/null +++ b/engine/renderapi/shader/details.go @@ -0,0 +1,50 @@ +package shader + +import ( + "encoding/json" + + "zworld/assets" + "zworld/engine/renderapi/texture" + "zworld/engine/renderapi/types" +) + +type InputDetails struct { + Index int + Type string +} + +type Details struct { + Inputs map[string]InputDetails + Bindings map[string]int + Textures []texture.Slot +} + +func (d *Details) ParseInputs() (Inputs, error) { + inputs := Inputs{} + for name, input := range d.Inputs { + kind, err := types.TypeFromString(input.Type) + if err != nil { + return nil, err + } + inputs[name] = Input{ + Index: input.Index, + Type: kind, + } + } + return inputs, nil +} + +func ReadDetails(path string) (*Details, error) { + data, err := assets.ReadAll(path) + if err != nil { + return nil, err + } + + details := &Details{} + err = json.Unmarshal(data, details) + if err != nil { + return nil, err + } + + return details, nil +} diff --git a/engine/renderapi/shader/module.go b/engine/renderapi/shader/module.go new file mode 100644 index 0000000..a702d68 --- /dev/null +++ b/engine/renderapi/shader/module.go @@ -0,0 +1,68 @@ +package shader + +import ( + "zworld/engine/renderapi/device" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/core/v2/driver" +) + +type Module interface { + device.Resource[core1_0.ShaderModule] + + Entrypoint() string + Stage() ShaderStage +} + +type shader_module struct { + device device.T + ptr core1_0.ShaderModule + stage ShaderStage +} + +func NewModule(device device.T, path string, stage ShaderStage) Module { + if device == nil { + panic("device is nil") + } + + bytecode, err := LoadOrCompile(path, stage) + if err != nil { + panic(err) + } + + ptr, result, err := device.Ptr().CreateShaderModule(nil, core1_0.ShaderModuleCreateInfo{ + Code: sliceUint32(bytecode), + }) + if err != nil { + panic(err) + } + if result != core1_0.VKSuccess { + panic("failed to create shader") + } + device.SetDebugObjectName(driver.VulkanHandle(ptr.Handle()), core1_0.ObjectTypeShaderModule, path) + + return &shader_module{ + device: device, + ptr: ptr, + stage: stage, + } +} + +func (b *shader_module) VkType() core1_0.ObjectType { return core1_0.ObjectTypeShaderModule } + +func (s *shader_module) Ptr() core1_0.ShaderModule { + return s.ptr +} + +func (s *shader_module) Stage() ShaderStage { + return s.stage +} + +func (s *shader_module) Entrypoint() string { + return "main" +} + +func (s *shader_module) Destroy() { + s.ptr.Destroy(nil) + s.ptr = nil +} diff --git a/engine/renderapi/shader/ref.go b/engine/renderapi/shader/ref.go new file mode 100644 index 0000000..00524e2 --- /dev/null +++ b/engine/renderapi/shader/ref.go @@ -0,0 +1,30 @@ +package shader + +import "zworld/engine/renderapi/device" + +type Ref interface { + Key() string + Version() int + + Load(device.T) T +} + +type ref struct { + name string +} + +func NewRef(name string) Ref { + return &ref{name: name} +} + +func (r *ref) Key() string { + return r.name +} + +func (r *ref) Version() int { + return 1 +} + +func (r *ref) Load(dev device.T) T { + return New(dev, r.name) +} diff --git a/engine/renderapi/shader/shader.go b/engine/renderapi/shader/shader.go new file mode 100644 index 0000000..c129324 --- /dev/null +++ b/engine/renderapi/shader/shader.go @@ -0,0 +1,101 @@ +package shader + +import ( + "fmt" + + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/texture" + "zworld/engine/renderapi/types" +) + +type Input struct { + Index int + Type types.Type +} + +type Inputs map[string]Input + +// Input returns the index and type of a shader input by name, and a bool indicating wheter its valid. +func (i Inputs) Input(name string) (int, types.Type, bool) { + input, exists := i[name] + return input.Index, input.Type, exists +} + +type Bindings map[string]int + +// Descriptor returns the index of a descriptor by name, and a bool indicating wheter its valid. +func (d Bindings) Descriptor(name string) (int, bool) { + index, exists := d[name] + return index, exists +} + +type T interface { + Name() string + Modules() []Module + Destroy() + Input(name string) (int, types.Type, bool) + Descriptor(name string) (int, bool) + Textures() []texture.Slot +} + +type shader struct { + name string + modules []Module + inputs Inputs + bindings Bindings + textures []texture.Slot +} + +func New(device device.T, path string) T { + // todo: inputs & descriptors should be obtained from SPIR-V reflection + details, err := ReadDetails(fmt.Sprintf("shaders/%s.json", path)) + if err != nil { + panic(fmt.Sprintf("failed to load shader details: %s", err)) + } + + inputs, err := details.ParseInputs() + if err != nil { + panic(fmt.Sprintf("failed to parse shader inputs: %s", err)) + } + + modules := []Module{ + NewModule(device, fmt.Sprintf("shaders/%s.vs.glsl", path), StageVertex), + NewModule(device, fmt.Sprintf("shaders/%s.fs.glsl", path), StageFragment), + } + + return &shader{ + name: path, + modules: modules, + inputs: inputs, + bindings: details.Bindings, + textures: details.Textures, + } +} + +// Name returns the file name of the shader +func (s *shader) Name() string { + return s.name +} + +func (s *shader) Modules() []Module { + return s.modules +} + +// Destroy the shader and its modules. +func (s *shader) Destroy() { + for _, module := range s.modules { + module.Destroy() + } +} + +func (s *shader) Input(name string) (int, types.Type, bool) { + return s.inputs.Input(name) +} + +func (s *shader) Textures() []texture.Slot { + return s.textures +} + +func (s *shader) Descriptor(name string) (int, bool) { + return s.bindings.Descriptor(name) +} diff --git a/engine/renderapi/shader/stage.go b/engine/renderapi/shader/stage.go new file mode 100644 index 0000000..e600f12 --- /dev/null +++ b/engine/renderapi/shader/stage.go @@ -0,0 +1,21 @@ +package shader + +import "github.com/vkngwrapper/core/v2/core1_0" + +type ShaderStage core1_0.ShaderStageFlags + +const ( + StageAll = ShaderStage(core1_0.StageAll) + StageVertex = ShaderStage(core1_0.StageVertex) + StageFragment = ShaderStage(core1_0.StageFragment) + StageCompute = ShaderStage(core1_0.StageCompute) +) + +func (s ShaderStage) String() string { + return s.flags().String() +} + +// flags returns the Vulkan-native representation +func (s ShaderStage) flags() core1_0.ShaderStageFlags { + return core1_0.ShaderStageFlags(s) +} diff --git a/engine/renderapi/shader/util.go b/engine/renderapi/shader/util.go new file mode 100644 index 0000000..bf2db73 --- /dev/null +++ b/engine/renderapi/shader/util.go @@ -0,0 +1,87 @@ +package shader + +import ( + "bytes" + "errors" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "unsafe" + + "zworld/assets" +) + +var ErrCompileFailed = errors.New("shader compilation error") + +// Disgusting hack that reinterprets a byte slice as a slice of uint32 +func sliceUint32(data []byte) []uint32 { + type sliceHeader struct { + Data uintptr + Len int + Cap int + } + const m = 0x7fffffff + return (*[m / 4]uint32)(unsafe.Pointer((*sliceHeader)(unsafe.Pointer(&data)).Data))[:len(data)/4] +} + +func LoadOrCompile(path string, stage ShaderStage) ([]byte, error) { + spvPath := fmt.Sprintf("%s.spv", path) + source, err := assets.ReadAll(spvPath) + if errors.Is(err, os.ErrNotExist) { + return Compile(path, stage) + } + if err != nil { + return nil, err + } + log.Println("loading shader", path) + return source, nil +} + +func Compile(path string, stage ShaderStage) ([]byte, error) { + stageflag := "" + switch stage { + case StageFragment: + stageflag = "-fshader-stage=fragment" + case StageVertex: + stageflag = "-fshader-stage=vertex" + case StageCompute: + stageflag = "-fshader-stage=compute" + } + + source, err := assets.ReadAll(path) + if err != nil { + return nil, err + } + + // todo: check for glslc + includePath := filepath.Join(assets.Path, "shaders") + bytecode := &bytes.Buffer{} + errors := &bytes.Buffer{} + args := []string{ + stageflag, + "-O", // optimize SPIR-V + "-I", includePath, // include path + "-o", "-", // output file: standard out + "-", // input file: standard in + } + cmd := exec.Command("glslc", args...) + cmd.Stdin = bytes.NewBuffer(source) + cmd.Stdout = bytecode + cmd.Stderr = errors + cmd.Dir = assets.Path + + if err := cmd.Run(); err != nil { + if errors.Len() > 0 { + return nil, fmt.Errorf("%w in %s:\n%s", + ErrCompileFailed, + path, + errors.String()) + } + return nil, fmt.Errorf("%s in %s: %w", ErrCompileFailed, path, err) + } + + log.Println("shader compiled successfully:", path) + return bytecode.Bytes(), nil +} diff --git a/engine/renderapi/swapchain/context.go b/engine/renderapi/swapchain/context.go new file mode 100644 index 0000000..1fef104 --- /dev/null +++ b/engine/renderapi/swapchain/context.go @@ -0,0 +1,54 @@ +package swapchain + +import ( + "fmt" + gosync "sync" + "time" + + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/sync" +) + +type Context struct { + Index int + Start time.Time + ImageAvailable sync.Semaphore + RenderComplete sync.Semaphore + + image int + inFlight *gosync.Mutex +} + +func newContext(dev device.T, index int) *Context { + return &Context{ + Index: index, + ImageAvailable: sync.NewSemaphore(dev, fmt.Sprintf("ImageAvailable:%d", index)), + RenderComplete: sync.NewSemaphore(dev, fmt.Sprintf("RenderComplete:%d", index)), + inFlight: &gosync.Mutex{}, + } +} + +func DummyContext() *Context { + return &Context{ + inFlight: &gosync.Mutex{}, + } +} + +func (c *Context) Destroy() { + if c.ImageAvailable != nil { + c.ImageAvailable.Destroy() + c.ImageAvailable = nil + } + if c.RenderComplete != nil { + c.RenderComplete.Destroy() + c.RenderComplete = nil + } +} + +func (c *Context) Aquire() { + c.inFlight.Lock() +} + +func (c *Context) Release() { + c.inFlight.Unlock() +} diff --git a/engine/renderapi/swapchain/swapchain.go b/engine/renderapi/swapchain/swapchain.go new file mode 100644 index 0000000..85b05fe --- /dev/null +++ b/engine/renderapi/swapchain/swapchain.go @@ -0,0 +1,208 @@ +package swapchain + +import ( + "fmt" + "log" + "time" + + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/image" + "zworld/engine/util" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/extensions/v2/khr_surface" + "github.com/vkngwrapper/extensions/v2/khr_swapchain" +) + +type T interface { + device.Resource[khr_swapchain.Swapchain] + + Aquire() (*Context, error) + Present(command.Worker, *Context) + Resize(int, int) + + Images() []image.T + SurfaceFormat() core1_0.Format +} + +type swapchain struct { + device device.T + ptr khr_swapchain.Swapchain + ext khr_swapchain.Extension + surface khr_surface.Surface + surfaceFmt khr_surface.SurfaceFormat + images []image.T + current int + frames int + width int + height int + resized bool + + contexts []*Context +} + +func New(device device.T, frames, width, height int, surface khr_surface.Surface, surfaceFormat khr_surface.SurfaceFormat) T { + s := &swapchain{ + device: device, + ext: khr_swapchain.CreateExtensionFromDevice(device.Ptr()), + surface: surface, + surfaceFmt: surfaceFormat, + frames: frames, + contexts: make([]*Context, frames), + width: width, + height: height, + } + s.create() + return s +} + +func (s *swapchain) Ptr() khr_swapchain.Swapchain { + return s.ptr +} + +func (s *swapchain) Images() []image.T { return s.images } +func (s *swapchain) SurfaceFormat() core1_0.Format { return core1_0.Format(s.surfaceFmt.Format) } + +func (s *swapchain) Resize(width, height int) { + // resizing actually happens the next time a frame is aquired + s.width = width + s.height = height + s.resized = true +} + +func (s *swapchain) recreate() { + log.Println("recreating swapchain") + + // wait for all in-flight frames + // no need to release locks, they will be destroyed + for _, ctx := range s.contexts { + ctx.Aquire() + } + + // wait for device idle + s.device.WaitIdle() + + // recreate swapchain resources + s.Destroy() + s.create() +} + +func (s *swapchain) create() { + imageFormat := core1_0.Format(s.surfaceFmt.Format) + imageUsage := core1_0.ImageUsageColorAttachment | core1_0.ImageUsageTransferSrc + imageSharing := core1_0.SharingModeExclusive + + swapInfo := khr_swapchain.SwapchainCreateInfo{ + Surface: s.surface, + MinImageCount: s.frames, + ImageFormat: imageFormat, + ImageColorSpace: khr_surface.ColorSpace(s.surfaceFmt.ColorSpace), + ImageExtent: core1_0.Extent2D{ + Width: s.width, + Height: s.height, + }, + ImageArrayLayers: 1, + ImageUsage: imageUsage, + ImageSharingMode: imageSharing, + PresentMode: khr_surface.PresentModeFIFO, + PreTransform: khr_surface.TransformIdentity, + CompositeAlpha: khr_surface.CompositeAlphaOpaque, + Clipped: true, + } + + var chain khr_swapchain.Swapchain + chain, _, err := s.ext.CreateSwapchain(s.device.Ptr(), nil, swapInfo) + if err != nil { + panic(err) + } + s.ptr = chain + + swapimages, result, err := chain.SwapchainImages() + if err != nil { + panic(err) + } + if result != core1_0.VKSuccess { + panic("failed to get swapchain images") + } + if len(swapimages) != s.frames { + panic("failed to get the requested number of swapchain images") + } + + // create images from swapchain buffers + s.images = util.Map(swapimages, func(img core1_0.Image) image.T { + return image.Wrap(s.device, img, image.Args{ + Type: core1_0.ImageType2D, + Width: s.width, + Height: s.height, + Depth: 1, + Levels: 1, + Format: imageFormat, + Usage: imageUsage, + Sharing: imageSharing, + }) + }) + + // create frame contexts + s.contexts = make([]*Context, len(s.images)) + for i := range s.contexts { + s.contexts[i] = newContext(s.device, i) + } + + // this ensures the first call to Aquire works properly + s.current = -1 +} + +func (s *swapchain) Aquire() (*Context, error) { + if s.resized { + s.recreate() + s.resized = false + return nil, fmt.Errorf("swapchain out of date") + } + + // get next frame context + s.current = (s.current + 1) % s.frames + ctx := s.contexts[s.current] + + // wait for frame context to become available + ctx.Aquire() + + idx, r, err := s.ptr.AcquireNextImage(time.Second, ctx.ImageAvailable.Ptr(), nil) + if err != nil { + panic(err) + } + if r == khr_swapchain.VKErrorOutOfDate { + s.recreate() + return nil, fmt.Errorf("swapchain out of date") + } + + // update swapchain output index + ctx.image = idx + + return ctx, nil +} + +func (s *swapchain) Present(worker command.Worker, ctx *Context) { + if ctx.RenderComplete == nil { + panic("context has no RenderComplete semaphore") + } + worker.Invoke(func() { + s.ext.QueuePresent(worker.Ptr(), khr_swapchain.PresentInfo{ + WaitSemaphores: []core1_0.Semaphore{ctx.RenderComplete.Ptr()}, + Swapchains: []khr_swapchain.Swapchain{s.Ptr()}, + ImageIndices: []int{ctx.image}, + }) + }) +} + +func (s *swapchain) Destroy() { + for _, context := range s.contexts { + context.Destroy() + } + s.contexts = nil + + if s.ptr != nil { + s.ptr.Destroy(nil) + s.ptr = nil + } +} diff --git a/engine/renderapi/sync/fence.go b/engine/renderapi/sync/fence.go new file mode 100644 index 0000000..d7164a0 --- /dev/null +++ b/engine/renderapi/sync/fence.go @@ -0,0 +1,77 @@ +package sync + +import ( + "time" + + "zworld/engine/renderapi/device" + "zworld/engine/util" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/core/v2/driver" +) + +type Fence interface { + device.Resource[core1_0.Fence] + + Reset() + Wait() + Done() bool +} + +type fence struct { + device device.T + ptr core1_0.Fence +} + +func NewFence(device device.T, name string, signaled bool) Fence { + var flags core1_0.FenceCreateFlags + if signaled { + flags = core1_0.FenceCreateSignaled + } + + ptr, _, err := device.Ptr().CreateFence(nil, core1_0.FenceCreateInfo{ + Flags: flags, + }) + if err != nil { + panic(err) + } + device.SetDebugObjectName(driver.VulkanHandle(ptr.Handle()), core1_0.ObjectTypeFence, name) + + return &fence{ + device: device, + ptr: ptr, + } +} + +func (f *fence) Ptr() core1_0.Fence { + return f.ptr +} + +func (f *fence) Reset() { + f.device.Ptr().ResetFences([]core1_0.Fence{f.ptr}) +} + +func (f *fence) Destroy() { + f.ptr.Destroy(nil) + f.ptr = nil +} + +func (f *fence) Wait() { + f.device.Ptr().WaitForFences(true, time.Hour, []core1_0.Fence{f.ptr}) +} + +func (f *fence) Done() bool { + r, err := f.ptr.Status() + if err != nil { + panic(err) + } + return r == core1_0.VKSuccess +} + +func (f *fence) WaitForAny(fences []Fence, timeout time.Duration) { + f.device.Ptr().WaitForFences(false, timeout, util.Map(fences, func(f Fence) core1_0.Fence { return f.Ptr() })) +} + +func (f *fence) WaitForAll(fences []Fence, timeout time.Duration) { + f.device.Ptr().WaitForFences(true, timeout, util.Map(fences, func(f Fence) core1_0.Fence { return f.Ptr() })) +} diff --git a/engine/renderapi/sync/mutex.go b/engine/renderapi/sync/mutex.go new file mode 100644 index 0000000..f8f85e1 --- /dev/null +++ b/engine/renderapi/sync/mutex.go @@ -0,0 +1,5 @@ +package sync + +import "sync" + +type Mutex sync.Mutex diff --git a/engine/renderapi/sync/semaphore.go b/engine/renderapi/sync/semaphore.go new file mode 100644 index 0000000..e574660 --- /dev/null +++ b/engine/renderapi/sync/semaphore.go @@ -0,0 +1,58 @@ +package sync + +import ( + "fmt" + + "zworld/engine/renderapi/device" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/core/v2/driver" +) + +type Semaphore interface { + device.Resource[core1_0.Semaphore] + Name() string +} + +type semaphore struct { + device device.T + ptr core1_0.Semaphore + name string +} + +func NewSemaphore(dev device.T, name string) Semaphore { + ptr, _, err := dev.Ptr().CreateSemaphore(nil, core1_0.SemaphoreCreateInfo{}) + if err != nil { + panic(err) + } + dev.SetDebugObjectName(driver.VulkanHandle(ptr.Handle()), core1_0.ObjectTypeSemaphore, name) + + return &semaphore{ + device: dev, + ptr: ptr, + name: name, + } +} + +func (s semaphore) Ptr() core1_0.Semaphore { + return s.ptr +} + +func (s *semaphore) Name() string { + return s.name +} + +func (s *semaphore) Destroy() { + if s.ptr != nil { + s.ptr.Destroy(nil) + s.ptr = nil + } +} + +func NewSemaphoreArray(dev device.T, name string, count int) []Semaphore { + arr := make([]Semaphore, count) + for i := range arr { + arr[i] = NewSemaphore(dev, fmt.Sprintf("%s:%d", name, i)) + } + return arr +} diff --git a/engine/renderapi/texture/const.go b/engine/renderapi/texture/const.go new file mode 100644 index 0000000..e59d5dd --- /dev/null +++ b/engine/renderapi/texture/const.go @@ -0,0 +1,14 @@ +package texture + +import "github.com/vkngwrapper/core/v2/core1_0" + +type Filter core1_0.Filter + +const FilterNearest = Filter(core1_0.FilterNearest) +const FilterLinear = Filter(core1_0.FilterLinear) + +type Wrap core1_0.SamplerAddressMode + +const WrapClamp = Wrap(core1_0.SamplerAddressModeClampToEdge) +const WrapRepeat = Wrap(core1_0.SamplerAddressModeRepeat) +const WrapMirror = Wrap(core1_0.SamplerAddressModeMirroredRepeat) diff --git a/engine/renderapi/texture/ref.go b/engine/renderapi/texture/ref.go new file mode 100644 index 0000000..602e117 --- /dev/null +++ b/engine/renderapi/texture/ref.go @@ -0,0 +1,60 @@ +package texture + +import ( + "zworld/engine/renderapi/image" +) + +var Checker = PathRef("textures/uv_checker.png") + +type Ref interface { + Key() string + Version() int + + // ImageData is called by texture caches and loaders, and should return the image data. + // todo: This interface is a bit too simple as it does not allow us to pass + // formats, filters and aspects. + ImageData() *image.Data + TextureArgs() Args +} + +type pathRef struct { + path string + img *image.Data + args Args +} + +func PathRef(path string) Ref { + return &pathRef{ + path: path, + args: Args{ + Filter: FilterLinear, + Wrap: WrapRepeat, + }, + } +} + +func PathArgsRef(path string, args Args) Ref { + return &pathRef{ + path: path, + args: args, + } +} + +func (r *pathRef) Key() string { return r.path } +func (r *pathRef) Version() int { return 1 } + +func (r *pathRef) ImageData() *image.Data { + if r.img != nil { + return r.img + } + var err error + r.img, err = image.LoadFile(r.path) + if err != nil { + panic(err) + } + return r.img +} + +func (r *pathRef) TextureArgs() Args { + return r.args +} diff --git a/engine/renderapi/texture/slot.go b/engine/renderapi/texture/slot.go new file mode 100644 index 0000000..06ad524 --- /dev/null +++ b/engine/renderapi/texture/slot.go @@ -0,0 +1,6 @@ +package texture + +type Slot string + +const Diffuse = Slot("diffuse") +const Normal = Slot("normal") diff --git a/engine/renderapi/texture/texture.go b/engine/renderapi/texture/texture.go new file mode 100644 index 0000000..1534d12 --- /dev/null +++ b/engine/renderapi/texture/texture.go @@ -0,0 +1,140 @@ +package texture + +import ( + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/image" + "zworld/engine/renderapi/vkerror" + "zworld/plugins/math/vec3" + + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/core/v2/driver" +) + +type T interface { + device.Resource[core1_0.Sampler] + Key() string + Image() image.T + View() image.View + Size() vec3.T +} + +type Args struct { + Filter Filter + Wrap Wrap + Aspect core1_0.ImageAspectFlags + Usage core1_0.ImageUsageFlags + Border core1_0.BorderColor +} + +type vktexture struct { + Args + ptr core1_0.Sampler + key string + device device.T + image image.T + view image.View +} + +func New(device device.T, key string, width, height int, format core1_0.Format, args Args) (T, error) { + if key == "" { + panic("texture must have a key") + } + args.Usage |= core1_0.ImageUsageFlags(core1_0.ImageUsageSampled | core1_0.ImageUsageTransferDst) + + img, err := image.New2D(device, key, width, height, format, args.Usage) + if err != nil { + return nil, err + } + + device.SetDebugObjectName(driver.VulkanHandle(img.Ptr().Handle()), + core1_0.ObjectTypeImage, key) + + tex, err := FromImage(device, key, img, args) + if err != nil { + img.Destroy() + return nil, err + } + + return tex, nil +} + +func FromImage(device device.T, key string, img image.T, args Args) (T, error) { + if key == "" { + key = img.Key() + } + if args.Aspect == 0 { + args.Aspect = core1_0.ImageAspectFlags(core1_0.ImageAspectColor) + } + + view, err := img.View(img.Format(), args.Aspect) + if err != nil { + return nil, err + } + + tex, err := FromView(device, key, view, args) + if err != nil { + // clean up + view.Destroy() + return nil, err + } + + return tex, nil +} + +func FromView(device device.T, key string, view image.View, args Args) (T, error) { + if key == "" { + panic("texture must have a key") + } + info := core1_0.SamplerCreateInfo{ + MinFilter: core1_0.Filter(args.Filter), + MagFilter: core1_0.Filter(args.Filter), + AddressModeU: core1_0.SamplerAddressMode(args.Wrap), + AddressModeV: core1_0.SamplerAddressMode(args.Wrap), + AddressModeW: core1_0.SamplerAddressMode(args.Wrap), + BorderColor: args.Border, + + MipmapMode: core1_0.SamplerMipmapModeLinear, + } + + ptr, result, err := device.Ptr().CreateSampler(nil, info) + if err != nil { + return nil, err + } + if result != core1_0.VKSuccess { + return nil, vkerror.FromResult(result) + } + + device.SetDebugObjectName(driver.VulkanHandle(ptr.Handle()), + core1_0.ObjectTypeSampler, key) + + return &vktexture{ + Args: args, + key: key, + ptr: ptr, + device: device, + image: view.Image(), + view: view, + }, nil +} + +func (t *vktexture) Ptr() core1_0.Sampler { + return t.ptr +} + +func (t *vktexture) Key() string { return t.key } +func (t *vktexture) Image() image.T { return t.image } +func (t *vktexture) View() image.View { return t.view } +func (t *vktexture) Size() vec3.T { return t.image.Size() } + +func (t *vktexture) Destroy() { + t.ptr.Destroy(nil) + t.ptr = nil + + t.view.Destroy() + t.view = nil + + t.image.Destroy() + t.image = nil + + t.device = nil +} diff --git a/engine/renderapi/types/types.go b/engine/renderapi/types/types.go new file mode 100644 index 0000000..b63a1bb --- /dev/null +++ b/engine/renderapi/types/types.go @@ -0,0 +1,159 @@ +package types + +import ( + "errors" + "fmt" +) + +// ErrUnknownType is returend when an illegal GL type name is used +var ErrUnknownType = errors.New("unknown data type") + +// Type holds OpenGL type constants +type Type uint32 + +// GL Type Constants +const ( + _ Type = iota + Bool + Int8 + UInt8 + Int16 + UInt16 + Int32 + UInt32 + Float + Vec2f + Vec3f + Vec4f + Mat3f + Mat4f + Double + Texture2D +) + +// Size returns the byte size of the GL type +func (t Type) Size() int { + switch t { + case Int8: + return 1 + case UInt8: + return 1 + case Int16: + return 2 + case UInt16: + return 2 + case Int32: + return 4 + case UInt32: + return 4 + case Float: + return 4 + case Double: + return 8 + } + panic(fmt.Errorf("unknown size for GL type %s", t)) +} + +func (t Type) String() string { + switch t { + case Bool: + return "bool" + case Int8: + return "int8" + case UInt8: + return "uint8" + case Int16: + return "int16" + case UInt16: + return "uint16" + case Int32: + return "int32" + case UInt32: + return "uint32" + case Float: + return "float" + case Double: + return "double" + case Vec2f: + return "vec2f" + case Vec3f: + return "vec3f" + case Vec4f: + return "vec4f" + case Mat3f: + return "mat3f" + case Mat4f: + return "mat4f" + case Texture2D: + return "tex2d" + default: + return fmt.Sprintf("unknown:%d", t) + } +} + +// Integer returns the if the type is an integer type +func (t Type) Integer() bool { + switch t { + case Float: + return false + case Vec2f: + return false + case Vec3f: + return false + case Vec4f: + return false + case Double: + return false + default: + return true + } +} + +// TypeFromString returns the GL identifier & size of a data type name +func TypeFromString(name string) (Type, error) { + switch name { + case "bool": + return Bool, nil + + case "byte": + fallthrough + case "int8": + return Int8, nil + + case "ubyte": + fallthrough + case "uint8": + return UInt8, nil + + case "short": + fallthrough + case "int16": + return Int16, nil + + case "ushort": + fallthrough + case "uint16": + return UInt16, nil + + case "int": + fallthrough + case "int32": + return Int32, nil + + case "uint": + fallthrough + case "uint32": + return UInt32, nil + + case "float": + fallthrough + case "float32": + return Float, nil + + case "float64": + fallthrough + case "double": + return Double, nil + } + return Type(0), ErrUnknownType +} diff --git a/engine/renderapi/upload/texture.go b/engine/renderapi/upload/texture.go new file mode 100644 index 0000000..7f4d62c --- /dev/null +++ b/engine/renderapi/upload/texture.go @@ -0,0 +1,173 @@ +package upload + +import ( + "fmt" + osimage "image" + "image/png" + "os" + + "zworld/engine/renderapi/buffer" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/image" + "zworld/engine/renderapi/texture" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +func NewTextureSync(dev device.T, worker command.Worker, key string, img *osimage.RGBA) (texture.T, error) { + // allocate texture + tex, err := texture.New(dev, + key, + img.Rect.Size().X, + img.Rect.Size().Y, + image.FormatRGBA8Unorm, + texture.Args{ + Filter: texture.FilterLinear, + Wrap: texture.WrapRepeat, + }) + if err != nil { + return nil, err + } + + // allocate staging buffer + stage := buffer.NewShared(dev, "staging:texture", len(img.Pix)) + + // write to staging buffer + stage.Write(0, img.Pix) + stage.Flush() + + // transfer data to texture buffer + worker.Queue(func(cmd command.Buffer) { + cmd.CmdImageBarrier( + core1_0.PipelineStageTopOfPipe, + core1_0.PipelineStageTransfer, + tex.Image(), + core1_0.ImageLayoutUndefined, + core1_0.ImageLayoutTransferDstOptimal, + core1_0.ImageAspectColor) + cmd.CmdCopyBufferToImage(stage, tex.Image(), core1_0.ImageLayoutTransferDstOptimal) + cmd.CmdImageBarrier( + core1_0.PipelineStageTransfer, + core1_0.PipelineStageFragmentShader, + tex.Image(), + core1_0.ImageLayoutTransferDstOptimal, + core1_0.ImageLayoutShaderReadOnlyOptimal, + core1_0.ImageAspectColor) + }) + worker.Submit(command.SubmitInfo{ + Marker: "TextureUpload", + Callback: stage.Destroy, + }) + worker.Flush() + + return tex, nil +} + +func DownloadImageAsync(dev device.T, worker command.Worker, src image.T) (<-chan *osimage.RGBA, error) { + swizzle := false + switch src.Format() { + case core1_0.FormatB8G8R8A8UnsignedNormalized: + swizzle = true + case core1_0.FormatR8G8B8A8UnsignedNormalized: + break + default: + return nil, fmt.Errorf("unsupported source format") + } + + dst, err := image.New(dev, image.Args{ + Type: core1_0.ImageType2D, + Width: src.Width(), + Height: src.Height(), + Depth: 1, + Layers: 1, + Levels: 1, + Format: core1_0.FormatR8G8B8A8UnsignedNormalized, + Memory: core1_0.MemoryPropertyHostVisible | core1_0.MemoryPropertyHostCoherent, + Tiling: core1_0.ImageTilingLinear, + Usage: core1_0.ImageUsageTransferDst, + Sharing: core1_0.SharingModeExclusive, + Layout: core1_0.ImageLayoutUndefined, + }) + if err != nil { + return nil, err + } + + // transfer data from texture buffer + worker.Queue(func(cmd command.Buffer) { + cmd.CmdImageBarrier( + core1_0.PipelineStageTopOfPipe, + core1_0.PipelineStageTransfer, + src, + core1_0.ImageLayoutUndefined, + core1_0.ImageLayoutTransferSrcOptimal, + core1_0.ImageAspectColor) + cmd.CmdImageBarrier( + core1_0.PipelineStageTopOfPipe, + core1_0.PipelineStageTransfer, + dst, + core1_0.ImageLayoutUndefined, + core1_0.ImageLayoutTransferDstOptimal, + core1_0.ImageAspectColor) + cmd.CmdCopyImage(src, core1_0.ImageLayoutTransferSrcOptimal, dst, core1_0.ImageLayoutTransferDstOptimal, core1_0.ImageAspectColor) + cmd.CmdImageBarrier( + core1_0.PipelineStageTransfer, + core1_0.PipelineStageBottomOfPipe, + src, + core1_0.ImageLayoutTransferSrcOptimal, + core1_0.ImageLayoutColorAttachmentOptimal, + core1_0.ImageAspectColor) + cmd.CmdImageBarrier( + core1_0.PipelineStageTopOfPipe, + core1_0.PipelineStageTransfer, + dst, + core1_0.ImageLayoutTransferDstOptimal, + core1_0.ImageLayoutGeneral, + core1_0.ImageAspectColor) + }) + + done := make(chan *osimage.RGBA) + worker.Submit(command.SubmitInfo{ + Marker: "TextureDownload", + Callback: func() { + defer dst.Destroy() + defer close(done) + + out := osimage.NewRGBA(osimage.Rect(0, 0, dst.Width(), dst.Height())) + dst.Memory().Read(0, out.Pix) + + // swizzle colors if required BGR -> RGB + if swizzle { + for i := 0; i < len(out.Pix); i += 4 { + b := out.Pix[i] + r := out.Pix[i+2] + out.Pix[i] = r + out.Pix[i+2] = b + } + } + done <- out + }, + }) + + return done, nil +} + +func DownloadImage(dev device.T, worker command.Worker, src image.T) (*osimage.RGBA, error) { + img, err := DownloadImageAsync(dev, worker, src) + if err != nil { + return nil, err + } + return <-img, nil +} + +func SavePng(img osimage.Image, filename string) error { + out, err := os.Create(filename) + if err != nil { + return nil + } + defer out.Close() + if err := png.Encode(out, img); err != nil { + return err + } + return nil +} diff --git a/engine/renderapi/vertex/cull_mode.go b/engine/renderapi/vertex/cull_mode.go new file mode 100644 index 0000000..3e3004f --- /dev/null +++ b/engine/renderapi/vertex/cull_mode.go @@ -0,0 +1,19 @@ +package vertex + +import "github.com/vkngwrapper/core/v2/core1_0" + +const ( + CullNone = CullMode(core1_0.CullModeFlags(0)) + CullFront = CullMode(core1_0.CullModeFront) + CullBack = CullMode(core1_0.CullModeBack) +) + +type CullMode core1_0.CullModeFlags + +func (c CullMode) flags() core1_0.CullModeFlags { + return core1_0.CullModeFlags(c) +} + +func (c CullMode) String() string { + return c.flags().String() +} diff --git a/engine/renderapi/vertex/format.go b/engine/renderapi/vertex/format.go new file mode 100644 index 0000000..61ee6c3 --- /dev/null +++ b/engine/renderapi/vertex/format.go @@ -0,0 +1,63 @@ +package vertex + +import ( + "zworld/engine/renderapi/color" + "zworld/plugins/math/vec2" + "zworld/plugins/math/vec3" + "zworld/plugins/math/vec4" +) + +// P - Position only vertex +type P struct { + vec3.T `vtx:"position,float,3"` +} + +func (v P) Position() vec3.T { return v.T } + +// C - Colored Vertex +type C struct { + P vec3.T `vtx:"position,float,3"` + N vec3.T `vtx:"normal,float,3"` + C vec4.T `vtx:"color_0,float,4"` +} + +func (v C) Position() vec3.T { return v.P } + +// T - Textured Vertex +type T struct { + P vec3.T `vtx:"position,float,3"` + N vec3.T `vtx:"normal,float,3"` + T vec2.T `vtx:"texcoord_0,float,2"` +} + +func (v T) Position() vec3.T { return v.P } + +type UI struct { + P vec3.T `vtx:"position,float,3"` + C color.T `vtx:"color_0,float,4"` + T vec2.T `vtx:"texcoord_0,float,2"` +} + +func (v UI) Position() vec3.T { return v.P } + +func Min[V Vertex](vertices []V) vec3.T { + if len(vertices) == 0 { + return vec3.Zero + } + min := vec3.InfPos + for _, v := range vertices { + min = vec3.Min(min, v.Position()) + } + return min +} + +func Max[V Vertex](vertices []V) vec3.T { + if len(vertices) == 0 { + return vec3.Zero + } + max := vec3.InfNeg + for _, v := range vertices { + max = vec3.Max(max, v.Position()) + } + return max +} diff --git a/engine/renderapi/vertex/index_type.go b/engine/renderapi/vertex/index_type.go new file mode 100644 index 0000000..eaa469b --- /dev/null +++ b/engine/renderapi/vertex/index_type.go @@ -0,0 +1,14 @@ +package vertex + +import "github.com/vkngwrapper/core/v2/core1_0" + +func IndexType(size int) core1_0.IndexType { + switch size { + case 2: + return core1_0.IndexTypeUInt16 + case 4: + return core1_0.IndexTypeUInt32 + default: + panic("illegal index size") + } +} diff --git a/engine/renderapi/vertex/mesh.go b/engine/renderapi/vertex/mesh.go new file mode 100644 index 0000000..ea7d681 --- /dev/null +++ b/engine/renderapi/vertex/mesh.go @@ -0,0 +1,135 @@ +package vertex + +import ( + "reflect" + + "zworld/plugins/math/vec3" +) + +type Mesh interface { + Key() string + Version() int + Primitive() Primitive + Pointers() Pointers + VertexCount() int + VertexData() any + VertexSize() int + IndexCount() int + IndexData() any + IndexSize() int + Min() vec3.T + Max() vec3.T + + Positions(func(vec3.T)) + Triangles(iter func(Triangle)) +} + +type Vertex interface { + Position() vec3.T +} + +type Index interface { + uint8 | uint16 | uint32 +} + +type MutableMesh[V Vertex, I Index] interface { + Mesh + Vertices() []V + Indices() []I + Update(vertices []V, indices []I) +} + +type mesh[V Vertex, I Index] struct { + key string + version int + indexsize int + vertexsize int + primitive Primitive + pointers Pointers + vertices []V + indices []I + min vec3.T + max vec3.T +} + +var _ Mesh = &mesh[P, uint8]{} + +func (m *mesh[V, I]) Key() string { return m.key } +func (m *mesh[V, I]) Version() int { return m.version } +func (m *mesh[V, I]) Primitive() Primitive { return m.primitive } +func (m *mesh[V, I]) Pointers() Pointers { return m.pointers } +func (m *mesh[V, I]) Vertices() []V { return m.vertices } +func (m *mesh[V, I]) VertexData() any { return m.vertices } +func (m *mesh[V, I]) VertexSize() int { return m.vertexsize } +func (m *mesh[V, I]) VertexCount() int { return len(m.vertices) } +func (m *mesh[V, I]) Indices() []I { return m.indices } +func (m *mesh[V, I]) IndexData() any { return m.indices } +func (m *mesh[V, I]) IndexSize() int { return m.indexsize } +func (m *mesh[V, I]) IndexCount() int { return len(m.indices) } +func (m *mesh[V, I]) String() string { return m.key } +func (m *mesh[V, I]) Min() vec3.T { return m.min } +func (m *mesh[V, I]) Max() vec3.T { return m.max } + +func (m *mesh[V, I]) Positions(iter func(vec3.T)) { + for _, index := range m.indices { + vertex := m.vertices[index] + iter(vertex.Position()) + } +} + +func (m *mesh[V, I]) Triangles(iter func(Triangle)) { + for i := 0; i+3 < len(m.indices); i += 3 { + iter(Triangle{ + A: m.vertices[m.indices[i+0]].Position(), + B: m.vertices[m.indices[i+1]].Position(), + C: m.vertices[m.indices[i+2]].Position(), + }) + } +} + +func (m *mesh[V, I]) Update(vertices []V, indices []I) { + if len(indices) == 0 { + indices = make([]I, len(vertices)) + for i := 0; i < len(indices); i++ { + indices[i] = I(i) + } + } + + // update mesh bounds + m.min = Min(vertices) + m.max = Max(vertices) + + m.vertices = vertices + m.indices = indices + m.version++ +} + +func NewMesh[V Vertex, I Index](key string, primitive Primitive, vertices []V, indices []I) MutableMesh[V, I] { + var vertex V + var index I + ptrs := ParsePointers(vertex) + + // calculate mesh bounds + min := Min(vertices) + max := Max(vertices) + + mesh := &mesh[V, I]{ + key: key, + pointers: ptrs, + vertexsize: ptrs.Stride(), + indexsize: int(reflect.TypeOf(index).Size()), + primitive: primitive, + min: min, + max: max, + } + mesh.Update(vertices, indices) + return mesh +} + +func NewTriangles[V Vertex, I Index](key string, vertices []V, indices []I) MutableMesh[V, I] { + return NewMesh(key, Triangles, vertices, indices) +} + +func NewLines[T Vertex, K Index](key string, vertices []T, indices []K) MutableMesh[T, K] { + return NewMesh(key, Lines, vertices, indices) +} diff --git a/engine/renderapi/vertex/mesh_generated.go b/engine/renderapi/vertex/mesh_generated.go new file mode 100644 index 0000000..16ff07f --- /dev/null +++ b/engine/renderapi/vertex/mesh_generated.go @@ -0,0 +1,28 @@ +package vertex + +type Args interface{} + +type GeneratedMesh[A Args, V Vertex, I Index] interface { + Mesh + Update(A) +} + +type generated[A Args, V Vertex, I Index] struct { + Mesh + key string + version int + hash int + generator func(A) (V, I) +} + +func NewGenerated[A Args, V Vertex, I Index](key string, args A, generator func(A) (V, I)) GeneratedMesh[A, V, I] { + return &generated[A, V, I]{ + key: key, + version: 1, + generator: generator, + } +} + +func (g *generated[A, V, I]) Update(args A) { + // if args hash has changed, update version +} diff --git a/engine/renderapi/vertex/optimize.go b/engine/renderapi/vertex/optimize.go new file mode 100644 index 0000000..d6f775f --- /dev/null +++ b/engine/renderapi/vertex/optimize.go @@ -0,0 +1,27 @@ +package vertex + +import "zworld/plugins/math/vec3" + +func CollisionMesh(mesh Mesh) Mesh { + // generate collision mesh + // todo: use greedy face optimization + + indexMap := make(map[vec3.T]uint32, mesh.IndexCount()) + vertexdata := make([]P, 0, mesh.VertexCount()/4) + indexdata := make([]uint32, 0, mesh.IndexCount()) + mesh.Positions(func(p vec3.T) { + // check if the vertex position already has an index + // todo: tolerance + index, exists := indexMap[p] + if !exists { + // create a new index from the vertex + index = uint32(len(vertexdata)) + vertexdata = append(vertexdata, P{p}) + indexMap[p] = index + } + // store vertex index + indexdata = append(indexdata, index) + }) + + return NewTriangles[P, uint32](mesh.Key(), vertexdata, indexdata) +} diff --git a/engine/renderapi/vertex/pointer.go b/engine/renderapi/vertex/pointer.go new file mode 100644 index 0000000..f60e7e2 --- /dev/null +++ b/engine/renderapi/vertex/pointer.go @@ -0,0 +1,21 @@ +package vertex + +import ( + "zworld/engine/renderapi/types" +) + +type Pointer struct { + Name string + Binding int + Source types.Type + Destination types.Type + Elements int + Stride int + Offset int + Normalize bool +} + +func (p *Pointer) Bind(binding int, kind types.Type) { + p.Binding = binding + p.Destination = kind +} diff --git a/engine/renderapi/vertex/pointers.go b/engine/renderapi/vertex/pointers.go new file mode 100644 index 0000000..eed507d --- /dev/null +++ b/engine/renderapi/vertex/pointers.go @@ -0,0 +1,21 @@ +package vertex + +import ( + "strings" + + "zworld/engine/util" +) + +type Pointers []Pointer + +func (ps Pointers) BufferString() string { + names := util.Map(ps, func(p Pointer) string { return p.Name }) + return strings.Join(names, ",") +} + +func (ps Pointers) Stride() int { + if len(ps) == 0 { + return 0 + } + return ps[0].Stride +} diff --git a/engine/renderapi/vertex/primitives.go b/engine/renderapi/vertex/primitives.go new file mode 100644 index 0000000..dfd300a --- /dev/null +++ b/engine/renderapi/vertex/primitives.go @@ -0,0 +1,11 @@ +package vertex + +import "github.com/vkngwrapper/core/v2/core1_0" + +type Primitive core1_0.PrimitiveTopology + +const ( + Triangles Primitive = Primitive(core1_0.PrimitiveTopologyTriangleList) + Lines = Primitive(core1_0.PrimitiveTopologyLineList) + Points = Primitive(core1_0.PrimitiveTopologyPointList) +) diff --git a/engine/renderapi/vertex/quad.go b/engine/renderapi/vertex/quad.go new file mode 100644 index 0000000..d1dec7a --- /dev/null +++ b/engine/renderapi/vertex/quad.go @@ -0,0 +1,19 @@ +package vertex + +import ( + "zworld/plugins/math/vec2" + "zworld/plugins/math/vec3" +) + +// Full-screen quad helper +func ScreenQuad(key string) Mesh { + return NewTriangles(key, []T{ + {P: vec3.New(-1, -1, 0), T: vec2.New(0, 0)}, + {P: vec3.New(1, 1, 0), T: vec2.New(1, 1)}, + {P: vec3.New(-1, 1, 0), T: vec2.New(0, 1)}, + {P: vec3.New(1, -1, 0), T: vec2.New(1, 0)}, + }, []uint16{ + 0, 1, 2, + 0, 3, 1, + }) +} diff --git a/engine/renderapi/vertex/tag.go b/engine/renderapi/vertex/tag.go new file mode 100644 index 0000000..d98aa30 --- /dev/null +++ b/engine/renderapi/vertex/tag.go @@ -0,0 +1,94 @@ +package vertex + +import ( + "fmt" + "reflect" + "strconv" + "strings" + + "zworld/engine/renderapi/types" +) + +type Tag struct { + Name string + Type string + Count int + Normalize bool +} + +func ParseTag(tag string) (Tag, error) { + p := strings.Split(tag, ",") + if len(p) < 3 || len(p) > 4 { + return Tag{}, fmt.Errorf("invalid vertex tag") + } + norm := false + name := strings.Trim(p[0], " ") + kind := strings.Trim(p[1], " ") + + count, err := strconv.Atoi(p[2]) + if err != nil { + return Tag{}, fmt.Errorf("expected count to be a number") + } + + if len(p) == 4 && p[3] == "normalize" { + norm = true + } + return Tag{ + Name: name, + Type: kind, + Count: count, + Normalize: norm, + }, nil +} + +func ParsePointers(data interface{}) Pointers { + var el reflect.Type + + t := reflect.TypeOf(data) + if t.Kind() == reflect.Struct { + el = t + } else if t.Kind() == reflect.Slice { + el = t.Elem() + } else { + panic("must be struct or slice") + } + + size := int(el.Size()) + + offset := 0 + pointers := make(Pointers, 0, el.NumField()) + for i := 0; i < el.NumField(); i++ { + f := el.Field(i) + tagstr := f.Tag.Get("vtx") + if tagstr == "skip" { + continue + } + tag, err := ParseTag(tagstr) + if err != nil { + fmt.Printf("tag error on %s.%s: %s\n", el.String(), f.Name, err) + continue + } + + kind, err := types.TypeFromString(tag.Type) + if err != nil { + panic(fmt.Errorf("invalid GL type: %s", tag.Type)) + } + + ptr := Pointer{ + Binding: -1, + Name: tag.Name, + Source: kind, + Destination: kind, + Elements: tag.Count, + Normalize: tag.Normalize, + Offset: offset, + Stride: size, + } + + pointers = append(pointers, ptr) + + offset += kind.Size() * tag.Count + } + + return pointers +} diff --git a/engine/renderapi/vertex/triangle.go b/engine/renderapi/vertex/triangle.go new file mode 100644 index 0000000..3c9ac3e --- /dev/null +++ b/engine/renderapi/vertex/triangle.go @@ -0,0 +1,23 @@ +package vertex + +import "zworld/plugins/math/vec3" + +type Triangle struct { + A, B, C vec3.T +} + +func (t *Triangle) Normal() vec3.T { + // Set Vector U to (Triangle.p2 minus Triangle.p1) + u := t.B.Sub(t.A) + // Set Vector V to (Triangle.p3 minus Triangle.p1) + v := t.C.Sub(t.A) + + // Set Normal.x to (multiply U.y by V.z) minus (multiply U.z by V.y) + x := u.Y*v.Z - u.Z*v.Y + // Set Normal.y to (multiply U.z by V.x) minus (multiply U.x by V.z) + y := u.Z*v.X - u.X*v.Z + // Set Normal.z to (multiply U.x by V.y) minus (multiply U.y by V.x) + z := u.X*v.Y - u.Y*v.X + + return vec3.New(x, y, z).Normalized() +} diff --git a/engine/renderapi/vertex/vertex_suite_test.go b/engine/renderapi/vertex/vertex_suite_test.go new file mode 100644 index 0000000..4c9eba6 --- /dev/null +++ b/engine/renderapi/vertex/vertex_suite_test.go @@ -0,0 +1,39 @@ +package vertex_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" + + "zworld/engine/renderapi/vertex" + "zworld/plugins/math/vec3" +) + +func TestVertex(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "renderapi/vertex") +} + +var _ = Describe("Optimize", func() { + It("correctly reduces the mesh", func() { + vertices := []vertex.P{ + {vec3.Zero}, + {vec3.Zero}, + {vec3.New(1, 1, 1)}, + {vec3.Zero}, + {vec3.One}, + } + indices := []uint32{ + 4, 1, 2, 3, 0, + } + + A := vertex.NewTriangles("test", vertices, indices) + C := vertex.CollisionMesh(A) + + m := C.(vertex.MutableMesh[vertex.P, uint32]) + Expect(m.Vertices()).To(HaveLen(2)) + Expect(m.Vertices()).To(Equal([]vertex.P{{vec3.One}, {vec3.Zero}})) + Expect(m.Indices()).To(Equal([]uint32{0, 1, 0, 1, 1})) + }) +}) diff --git a/engine/renderapi/vkerror/errors.go b/engine/renderapi/vkerror/errors.go new file mode 100644 index 0000000..cbeeed1 --- /dev/null +++ b/engine/renderapi/vkerror/errors.go @@ -0,0 +1,28 @@ +package vkerror + +import ( + "errors" + "fmt" + + "github.com/vkngwrapper/core/v2/common" + "github.com/vkngwrapper/core/v2/core1_0" +) + +var ErrOutOfHostMemory = errors.New("out of host memory") +var ErrOutOfDeviceMemory = errors.New("out of device memory") + +func FromResult(result common.VkResult) error { + switch result { + case core1_0.VKSuccess: + return nil + + case core1_0.VKErrorOutOfHostMemory: + return ErrOutOfHostMemory + + case core1_0.VKErrorOutOfDeviceMemory: + return ErrOutOfDeviceMemory + + default: + return fmt.Errorf("unmapped Vulkan error: %d", result) + } +} diff --git a/engine/renderapi/vulkan/backend.go b/engine/renderapi/vulkan/backend.go new file mode 100644 index 0000000..0233cc9 --- /dev/null +++ b/engine/renderapi/vulkan/backend.go @@ -0,0 +1,131 @@ +package vulkan + +import ( + "fmt" + + "zworld/engine/renderapi/cache" + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/descriptor" + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/vulkan/instance" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +type App interface { + Instance() instance.T + Device() device.T + Destroy() + + Worker(int) command.Worker + Transferer() command.Worker + Flush() + + Pool() descriptor.Pool + Meshes() cache.MeshCache + Textures() cache.TextureCache + Shaders() cache.ShaderCache +} + +type backend struct { + appName string + instance instance.T + device device.T + + transfer command.Worker + workers []command.Worker + + pool descriptor.Pool + meshes cache.MeshCache + textures cache.TextureCache + shaders cache.ShaderCache +} + +func New(appName string, deviceIndex int) App { + instance := instance.New(appName) + device, err := device.New(instance, instance.EnumeratePhysicalDevices()[0]) + if err != nil { + panic(err) + } + + // transfer worker + transfer := command.NewWorker(device, "xfer", core1_0.QueueTransfer|core1_0.QueueGraphics, 0) + + // per frame graphics workers + workerCount := 1 // frames + workers := make([]command.Worker, workerCount) + for i := range workers { + workers[i] = command.NewWorker(device, fmt.Sprintf("frame%d", i), core1_0.QueueGraphics, i+1) + } + + // init caches + meshes := cache.NewMeshCache(device, transfer) + textures := cache.NewTextureCache(device, transfer) + shaders := cache.NewShaderCache(device) + + pool := descriptor.NewPool(device, 1000, DefaultDescriptorPools) + + return &backend{ + appName: appName, + + device: device, + instance: instance, + transfer: transfer, + workers: workers, + meshes: meshes, + textures: textures, + shaders: shaders, + pool: pool, + } +} + +func (b *backend) Instance() instance.T { return b.instance } +func (b *backend) Device() device.T { return b.device } + +func (b *backend) Pool() descriptor.Pool { return b.pool } +func (b *backend) Meshes() cache.MeshCache { return b.meshes } +func (b *backend) Textures() cache.TextureCache { return b.textures } +func (b *backend) Shaders() cache.ShaderCache { return b.shaders } + +func (b *backend) Transferer() command.Worker { + return b.transfer +} + +func (b *backend) Worker(frame int) command.Worker { + return b.workers[frame%len(b.workers)] +} + +func (b *backend) Flush() { + // wait for all workers + for _, w := range b.workers { + w.Flush() + } + b.device.WaitIdle() +} + +func (b *backend) Destroy() { + // flush any pending work + b.Flush() + + // clean up caches + b.pool.Destroy() + b.meshes.Destroy() + b.textures.Destroy() + b.shaders.Destroy() + + // destroy workers + b.transfer.Destroy() + for _, w := range b.workers { + w.Destroy() + } + b.workers = nil + + if b.device != nil { + b.device.Destroy() + b.device = nil + } + if b.instance != nil { + b.instance.Destroy() + b.instance = nil + } +} diff --git a/engine/renderapi/vulkan/init.go b/engine/renderapi/vulkan/init.go new file mode 100644 index 0000000..5bd116c --- /dev/null +++ b/engine/renderapi/vulkan/init.go @@ -0,0 +1,17 @@ +package vulkan + +import ( + "runtime" + + "github.com/go-gl/glfw/v3.3/glfw" +) + +func init() { + // glfw event handling must run on the main OS thread + runtime.LockOSThread() + + // init glfw + if err := glfw.Init(); err != nil { + panic(err) + } +} diff --git a/engine/renderapi/vulkan/instance/instance.go b/engine/renderapi/vulkan/instance/instance.go new file mode 100644 index 0000000..49e2b3f --- /dev/null +++ b/engine/renderapi/vulkan/instance/instance.go @@ -0,0 +1,61 @@ +package instance + +//#cgo CFLAGS: -IF:/Coding/GoModule/cgo/include +//#cgo LDFLAGS: -LF:/Coding/GoModule/cgo/lib -lvulkan +// +import "C" +import ( + "github.com/go-gl/glfw/v3.3/glfw" + "github.com/vkngwrapper/core/v2" + "github.com/vkngwrapper/core/v2/common" + "github.com/vkngwrapper/core/v2/core1_0" +) + +type T interface { + EnumeratePhysicalDevices() []core1_0.PhysicalDevice + Destroy() + Ptr() core1_0.Instance +} + +type instance struct { + ptr core1_0.Instance +} + +func New(appName string) T { + loader, err := core.CreateLoaderFromProcAddr(glfw.GetVulkanGetInstanceProcAddress()) + if err != nil { + panic(err) + } + handle, _, err := loader.CreateInstance(nil, core1_0.InstanceCreateInfo{ + APIVersion: common.APIVersion(common.CreateVersion(1, 1, 0)), + ApplicationName: appName, + ApplicationVersion: common.CreateVersion(0, 1, 0), + EngineName: engineName, + EngineVersion: common.CreateVersion(0, 2, 1), + EnabledLayerNames: _EnabledLayerNames(), + EnabledExtensionNames: _EnabledExtensionNames(loader), + }) + if err != nil { + panic(err) + } + return &instance{ + ptr: handle, + } +} + +func (i *instance) Ptr() core1_0.Instance { + return i.ptr +} + +func (i *instance) Destroy() { + i.ptr.Destroy(nil) + i.ptr = nil +} + +func (i *instance) EnumeratePhysicalDevices() []core1_0.PhysicalDevice { + r, _, err := i.ptr.EnumeratePhysicalDevices() + if err != nil { + panic(err) + } + return r +} diff --git a/engine/renderapi/vulkan/instance/instance_help.go b/engine/renderapi/vulkan/instance/instance_help.go new file mode 100644 index 0000000..b2399b0 --- /dev/null +++ b/engine/renderapi/vulkan/instance/instance_help.go @@ -0,0 +1,43 @@ +package instance + +import ( + "github.com/vkngwrapper/core/v2" + "github.com/vkngwrapper/extensions/v2/ext_debug_utils" + "github.com/vkngwrapper/extensions/v2/khr_get_physical_device_properties2" + "github.com/vkngwrapper/extensions/v2/khr_surface" + "log" +) + +var ( + extensions = []string{ + khr_surface.ExtensionName, + ext_debug_utils.ExtensionName, + khr_get_physical_device_properties2.ExtensionName, + "VK_KHR_win32_surface", + "VK_EXT_debug_report", + "VK_KHR_portability_enumeration", + } + layers = []string{ + "VK_LAYER_KHRONOS_validation", + //"VK_LAYER_LUNARG_api_dump", + } + engineName = "goworld" +) + +func _EnabledExtensionNames(loader *core.VulkanLoader) []string { + availableExtensions, _, _ := loader.AvailableExtensions() + var _extensions []string + for _, ext := range extensions { + _, hasExt := availableExtensions[ext] + if !hasExt { + log.Printf("cann't support extension: %s", ext) + continue + } + _extensions = append(_extensions, ext) + } + extensions = _extensions + return _extensions +} +func _EnabledLayerNames() []string { + return layers +} diff --git a/engine/renderapi/vulkan/pool.go b/engine/renderapi/vulkan/pool.go new file mode 100644 index 0000000..e66cf80 --- /dev/null +++ b/engine/renderapi/vulkan/pool.go @@ -0,0 +1,22 @@ +package vulkan + +import "github.com/vkngwrapper/core/v2/core1_0" + +var DefaultDescriptorPools = []core1_0.DescriptorPoolSize{ + { + Type: core1_0.DescriptorTypeUniformBuffer, + DescriptorCount: 10000, + }, + { + Type: core1_0.DescriptorTypeStorageBuffer, + DescriptorCount: 10000, + }, + { + Type: core1_0.DescriptorTypeCombinedImageSampler, + DescriptorCount: 100000, + }, + { + Type: core1_0.DescriptorTypeInputAttachment, + DescriptorCount: 1000, + }, +} diff --git a/engine/renderapi/vulkan/target.go b/engine/renderapi/vulkan/target.go new file mode 100644 index 0000000..09cb2ef --- /dev/null +++ b/engine/renderapi/vulkan/target.go @@ -0,0 +1,110 @@ +package vulkan + +import ( + "fmt" + + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/device" + "zworld/engine/renderapi/image" + "zworld/engine/renderapi/swapchain" + + "github.com/vkngwrapper/core/v2/core1_0" +) + +// the renderapi target interfaces & implementations probably dont belong in this package long-term + +type TargetSize struct { + Width int + Height int + Frames int + Scale float32 +} + +type Target interface { + Size() TargetSize + Scale() float32 + Width() int + Height() int + Frames() int + + Surfaces() []image.T + SurfaceFormat() core1_0.Format + Aquire() (*swapchain.Context, error) + Present(command.Worker, *swapchain.Context) + + Destroy() +} + +type renderTarget struct { + size TargetSize + format core1_0.Format + usage core1_0.ImageUsageFlags + surfaces []image.T + context *swapchain.Context +} + +func NewDepthTarget(device device.T, key string, size TargetSize) Target { + format := device.GetDepthFormat() + usage := core1_0.ImageUsageSampled | core1_0.ImageUsageDepthStencilAttachment | core1_0.ImageUsageInputAttachment + target, err := NewRenderTarget(device, key, format, usage, size) + if err != nil { + panic(err) + } + return target +} + +func NewColorTarget(device device.T, key string, format core1_0.Format, size TargetSize) Target { + usage := core1_0.ImageUsageSampled | core1_0.ImageUsageColorAttachment | core1_0.ImageUsageInputAttachment | core1_0.ImageUsageTransferSrc + target, err := NewRenderTarget(device, key, format, usage, size) + if err != nil { + panic(err) + } + return target +} + +func NewRenderTarget(device device.T, key string, format core1_0.Format, usage core1_0.ImageUsageFlags, size TargetSize) (Target, error) { + var err error + outputs := make([]image.T, size.Frames) + for i := 0; i < size.Frames; i++ { + outputs[i], err = image.New2D(device, fmt.Sprintf("%s:%d", key, i), size.Width, size.Height, format, usage) + if err != nil { + return nil, err + } + } + + return &renderTarget{ + size: size, + format: format, + usage: usage, + surfaces: outputs, + context: swapchain.DummyContext(), + }, nil +} + +func (r *renderTarget) Frames() int { return len(r.surfaces) } +func (r *renderTarget) Width() int { return r.size.Width } +func (r *renderTarget) Height() int { return r.size.Height } +func (r *renderTarget) Scale() float32 { return r.size.Scale } + +func (r *renderTarget) Size() TargetSize { + return r.size +} + +func (r *renderTarget) Destroy() { + for _, output := range r.surfaces { + output.Destroy() + } + r.surfaces = nil +} + +func (i *renderTarget) Surfaces() []image.T { return i.surfaces } +func (i *renderTarget) SurfaceFormat() core1_0.Format { return i.format } + +func (i *renderTarget) Aquire() (*swapchain.Context, error) { + i.context.Aquire() + return i.context, nil +} + +func (b *renderTarget) Present(command.Worker, *swapchain.Context) { + +} diff --git a/engine/renderapi/vulkan/util.go b/engine/renderapi/vulkan/util.go new file mode 100644 index 0000000..6412588 --- /dev/null +++ b/engine/renderapi/vulkan/util.go @@ -0,0 +1,30 @@ +package vulkan + +import ( + "github.com/go-gl/glfw/v3.3/glfw" + "zworld/plugins/math" +) + +func GetCurrentMonitor(window *glfw.Window) *glfw.Monitor { + // translated to Go from https://stackoverflow.com/a/31526753 + wx, wy := window.GetPos() + ww, wh := window.GetSize() + + bestoverlap := 0 + var bestmonitor *glfw.Monitor + for _, monitor := range glfw.GetMonitors() { + mode := monitor.GetVideoMode() + mx, my := monitor.GetPos() + mw, mh := mode.Width, mode.Height + + overlap := math.Max(0, math.Min(wx+ww, mx+mw)-math.Max(wx, mx)) * + math.Max(0, math.Min(wy+wh, my+mh)-math.Max(wy, my)) + + if bestoverlap < overlap { + bestoverlap = overlap + bestmonitor = monitor + } + } + + return bestmonitor +} diff --git a/engine/renderapi/vulkan/window.go b/engine/renderapi/vulkan/window.go new file mode 100644 index 0000000..7dca4f5 --- /dev/null +++ b/engine/renderapi/vulkan/window.go @@ -0,0 +1,185 @@ +package vulkan + +import ( + "fmt" + "log" + "unsafe" + + "zworld/engine/renderapi/command" + "zworld/engine/renderapi/image" + "zworld/engine/renderapi/swapchain" + "zworld/plugins/system/input" + "zworld/plugins/system/input/keys" + "zworld/plugins/system/input/mouse" + + "github.com/go-gl/glfw/v3.3/glfw" + "github.com/vkngwrapper/core/v2/core1_0" + "github.com/vkngwrapper/core/v2/driver" + "github.com/vkngwrapper/extensions/v2/khr_surface" + khr_surface_driver "github.com/vkngwrapper/extensions/v2/khr_surface/driver" +) + +type ResizeHandler func(width, height int) + +type Window interface { + Target + + Title() string + SetTitle(string) + + Poll() + ShouldClose() bool + Destroy() + + SetInputHandler(input.Handler) + + Swapchain() swapchain.T +} + +type WindowArgs struct { + Title string + Width int + Height int + Frames int + Vsync bool + Debug bool + InputHandler input.Handler + ResizeHandler ResizeHandler +} + +type window struct { + wnd *glfw.Window + mouse mouse.MouseWrapper + + title string + width, height int + frames int + scale float32 + swap swapchain.T + surface khr_surface.Surface +} + +func NewWindow(backend App, args WindowArgs) (Window, error) { + // window creation hints. + glfw.WindowHint(glfw.ClientAPI, glfw.NoAPI) + + // create a new GLFW window + wnd, err := glfw.CreateWindow(args.Width, args.Height, args.Title, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to create glfw window: %w", err) + } + + // find the scaling of the current monitor + // what do we do if the user moves it to a different monitor with different scaling? + monitor := GetCurrentMonitor(wnd) + scale, _ := monitor.GetContentScale() + + // retrieve window & framebuffer size + width, height := wnd.GetFramebufferSize() + log.Printf("Created window with size %dx%d and content scale %.0f%%\n", + width, height, scale*100) + + // create window surface + surfPtr, err := wnd.CreateWindowSurface((*driver.VkInstance)(unsafe.Pointer(backend.Instance().Ptr().Handle())), nil) + if err != nil { + panic(err) + } + + surfaceHandle := (*khr_surface_driver.VkSurfaceKHR)(unsafe.Pointer(surfPtr)) + surfaceExt := khr_surface.CreateExtensionFromInstance(backend.Instance().Ptr()) + surface, err := surfaceExt.CreateSurfaceFromHandle(*surfaceHandle) + if err != nil { + panic(err) + } + + surfaceFormat, _, err := surface.PhysicalDeviceSurfaceFormats(backend.Device().Physical()) + if err != nil { + panic(err) + } + + // allocate swapchain + swap := swapchain.New(backend.Device(), args.Frames, width, height, surface, surfaceFormat[0]) + + window := &window{ + wnd: wnd, + title: args.Title, + width: width, + height: height, + frames: args.Frames, + scale: scale, + swap: swap, + surface: surface, + } + + // attach default input handler, if provided + if args.InputHandler != nil { + window.SetInputHandler(args.InputHandler) + } + + // set resize callback + wnd.SetFramebufferSizeCallback(func(w *glfw.Window, width, height int) { + // update window scaling + monitor := GetCurrentMonitor(wnd) + window.scale, _ = monitor.GetContentScale() + + window.width = width + window.height = height + window.swap.Resize(width, height) + }) + + return window, nil +} + +func (w *window) Poll() { + glfw.PollEvents() +} + +func (w *window) Size() TargetSize { + return TargetSize{ + Width: w.width, + Height: w.height, + Frames: w.frames, + Scale: w.scale, + } +} + +func (w *window) Width() int { return w.width } +func (w *window) Height() int { return w.height } +func (w *window) Frames() int { return w.frames } +func (w *window) Scale() float32 { return w.scale } +func (w *window) ShouldClose() bool { return w.wnd.ShouldClose() } +func (w *window) Title() string { return w.title } + +func (w *window) Surfaces() []image.T { return w.swap.Images() } +func (w *window) SurfaceFormat() core1_0.Format { return w.swap.SurfaceFormat() } +func (w *window) Swapchain() swapchain.T { return w.swap } + +func (w *window) SetInputHandler(handler input.Handler) { + // keyboard events + w.wnd.SetKeyCallback(keys.KeyCallbackWrapper(handler)) + w.wnd.SetCharCallback(keys.CharCallbackWrapper(handler)) + + // mouse events + w.mouse = mouse.NewWrapper(handler) + w.wnd.SetMouseButtonCallback(w.mouse.Button) + w.wnd.SetCursorPosCallback(w.mouse.Move) + w.wnd.SetScrollCallback(w.mouse.Scroll) +} + +func (w *window) SetTitle(title string) { + w.wnd.SetTitle(title) + w.title = title +} + +func (w *window) Aquire() (*swapchain.Context, error) { + return w.swap.Aquire() +} + +func (w *window) Present(worker command.Worker, ctx *swapchain.Context) { + w.swap.Present(worker, ctx) +} + +func (w *window) Destroy() { + w.swap.Destroy() + w.surface.Destroy(nil) +} diff --git a/engine/run.go b/engine/run.go new file mode 100644 index 0000000..a19cb4a --- /dev/null +++ b/engine/run.go @@ -0,0 +1,81 @@ +package engine + +import ( + "log" + "runtime" + "time" + "zworld/engine/object" + "zworld/engine/render/graph" + "zworld/engine/renderapi/vulkan" +) + +type SceneFunc func(object.Object) +type RendererFunc func(vulkan.App, vulkan.Target) graph.T + +type Args struct { + Title string + Width int + Height int + Renderer RendererFunc +} + +func Run(args Args, scenefuncs ...SceneFunc) { + log.Println("goworld") + runtime.LockOSThread() + + go RunProfilingServer(6060) + interrupt := NewInterrupter() + + backend := vulkan.New("goworld", 0) + defer backend.Destroy() + + if args.Renderer == nil { + args.Renderer = graph.Default + } + + // create a window + wnd, err := vulkan.NewWindow(backend, vulkan.WindowArgs{ + Title: args.Title, + Width: args.Width, + Height: args.Height, + Frames: 3, + }) + if err != nil { + panic(err) + } + defer wnd.Destroy() + + // create renderer + renderer := args.Renderer(backend, wnd) + defer renderer.Destroy() + + // create scene + scene := object.Empty("Scene") + wnd.SetInputHandler(scene) + for _, scenefunc := range scenefuncs { + scenefunc(scene) + } + + // run the render loop + log.Println("ready") + + counter := NewFrameCounter(60) + for interrupt.Running() && !wnd.ShouldClose() { + // update scene + wnd.Poll() + counter.Update() + scene.Update(scene, counter.Delta()) + + // draw + renderer.Draw(scene, counter.Elapsed(), counter.Delta()) + } +} + +func RunGC() { + start := time.Now() + runtime.GC() + elapsed := time.Since(start) + if elapsed.Milliseconds() > 1 { + log.Printf("slow GC cycle: %.2fms", elapsed.Seconds()*1000) + } +} diff --git a/engine/util/align.go b/engine/util/align.go new file mode 100644 index 0000000..89bf6b6 --- /dev/null +++ b/engine/util/align.go @@ -0,0 +1,35 @@ +package util + +import ( + "fmt" + "reflect" +) + +// ValidateAlignment checks if a given struct shares the memory layout of an equivalent C struct +func ValidateAlignment(value any) error { + t := reflect.TypeOf(value) + if t.Kind() != reflect.Struct { + return fmt.Errorf("value must be a struct, was %s", t.Kind()) + } + + expectedOffset := 0 + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if field.Offset != uintptr(expectedOffset) { + return fmt.Errorf("layout causes alignment issues. expected field %s to have offset %d, was %d", + field.Name, expectedOffset, field.Offset) + } + expectedOffset = int(field.Offset + field.Type.Size()) + } + + return nil +} + +func Align(offset, alignment int) int { + count := offset / alignment + diff := offset % alignment + if diff > 0 { + count++ + } + return count * alignment +} diff --git a/engine/util/align_test.go b/engine/util/align_test.go new file mode 100644 index 0000000..7584b2d --- /dev/null +++ b/engine/util/align_test.go @@ -0,0 +1,46 @@ +package util_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + . "github.com/johanhenriksson/goworld/util" +) + +type AlignCase struct { + Offset int + Alignment int + Expected int +} + +var _ = Describe("align utils", func() { + It("returns the expected alignment", func() { + cases := []AlignCase{ + {23, 64, 64}, + {64, 64, 64}, + {72, 64, 128}, + } + for _, testcase := range cases { + actual := Align(testcase.Offset, testcase.Alignment) + Expect(actual).To(Equal(testcase.Expected)) + } + }) + + It("returns errors for misaligned structs", func() { + type FailingStruct struct { + A bool + B int + } + err := ValidateAlignment(FailingStruct{}) + Expect(err).To(HaveOccurred()) + }) + + It("validates aligned structs", func() { + type PassingStruct struct { + A int + B float32 + } + err := ValidateAlignment(PassingStruct{}) + Expect(err).ToNot(HaveOccurred()) + }) +}) diff --git a/engine/util/map.go b/engine/util/map.go new file mode 100644 index 0000000..ce105bd --- /dev/null +++ b/engine/util/map.go @@ -0,0 +1,21 @@ +package util + +func MapValues[K comparable, V any, T any](m map[K]V, transform func(V) T) []T { + output := make([]T, len(m)) + i := 0 + for _, value := range m { + output[i] = transform(value) + i++ + } + return output +} + +func MapKeys[K comparable, V any, T any](m map[K]V, transform func(K) T) []T { + output := make([]T, len(m)) + i := 0 + for key := range m { + output[i] = transform(key) + i++ + } + return output +} diff --git a/engine/util/slice.go b/engine/util/slice.go new file mode 100644 index 0000000..f1fb82b --- /dev/null +++ b/engine/util/slice.go @@ -0,0 +1,68 @@ +package util + +func Map[T any, S any](items []T, transform func(T) S) []S { + output := make([]S, len(items)) + for i, item := range items { + output[i] = transform(item) + } + return output +} + +func MapIdx[T any, S any](items []T, transform func(T, int) S) []S { + output := make([]S, len(items)) + for i, item := range items { + output[i] = transform(item, i) + } + return output +} + +func Range(from, to, step int) []int { + n := (to - from) / step + output := make([]int, n) + v := from + for i := 0; v < to; i++ { + output[i] = v + v += step + } + return output +} + +func Chunks[T any](slice []T, size int) [][]T { + count := len(slice) / size + chunks := make([][]T, 0, count) + for i := 0; i < len(slice); i += size { + end := i + size + if end > len(slice) { + end = len(slice) + } + chunks = append(chunks, slice[i:end]) + } + return chunks +} + +func Reduce[T any, S any](slice []T, initial S, reducer func(S, T) S) S { + accumulator := initial + for _, item := range slice { + accumulator = reducer(accumulator, item) + } + return accumulator +} + +func Filter[T any](slice []T, predicate func(T) bool) []T { + output := make([]T, 0, len(slice)) + for _, item := range slice { + if predicate(item) { + output = append(output, item) + } + } + return output +} + +func Contains[T comparable](slice []T, element T) bool { + for _, item := range slice { + if item == element { + return true + } + } + return false +} diff --git a/engine/util/strings.go b/engine/util/strings.go new file mode 100644 index 0000000..f61e3fa --- /dev/null +++ b/engine/util/strings.go @@ -0,0 +1,16 @@ +package util + +import "strings" + +func CStrings(strings []string) []string { + return Map(strings, func(str string) string { + return CString(str) + }) +} + +func CString(str string) string { + if strings.HasSuffix(str, "\x00") { + return str + } + return str + "\x00" +} diff --git a/engine/util/sync_map.go b/engine/util/sync_map.go new file mode 100644 index 0000000..3f20687 --- /dev/null +++ b/engine/util/sync_map.go @@ -0,0 +1,30 @@ +package util + +import ( + "sync" +) + +// Type-safe sync.Map implementation +// Read sync.Map documentation for caveats +type SyncMap[K comparable, V any] struct { + m sync.Map +} + +func NewSyncMap[K comparable, V any]() *SyncMap[K, V] { + return &SyncMap[K, V]{ + m: sync.Map{}, + } +} + +func (m *SyncMap[K, V]) Load(key K) (value V, exists bool) { + var v any + v, exists = m.m.Load(key) + if exists { + value = v.(V) + } + return +} + +func (m *SyncMap[K, V]) Store(key K, value V) { + m.m.Store(key, value) +} diff --git a/engine/util/timer.go b/engine/util/timer.go new file mode 100644 index 0000000..b9ba86a --- /dev/null +++ b/engine/util/timer.go @@ -0,0 +1,21 @@ +package util + +import ( + "log" + "time" +) + +var timers = map[string]time.Time{} + +func Timer(name string) { + timers[name] = time.Now() +} + +func Elapsed(name string) float32 { + if start, exists := timers[name]; exists { + dt := float32(time.Since(start).Seconds()) + log.Printf("Elapsed %s=%.2fms\n", name, dt*1000) + return dt + } + return 0 +} diff --git a/engine/util/util_suite_test.go b/engine/util/util_suite_test.go new file mode 100644 index 0000000..1805d42 --- /dev/null +++ b/engine/util/util_suite_test.go @@ -0,0 +1,13 @@ +package util_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" +) + +func TestUtils(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "util") +} diff --git a/engine/util/uuid.go b/engine/util/uuid.go new file mode 100644 index 0000000..07947ce --- /dev/null +++ b/engine/util/uuid.go @@ -0,0 +1,17 @@ +package util + +import ( + "math/rand" +) + +var idCharset = []byte("abcdefghijklmnopqrstuvxyzABCDEFGHIJKLMNOPQRSTUVXYZ0123456789") + +func NewUUID(length int) string { + id := make([]byte, length) + charsetLen := int64(len(idCharset)) + for i := 0; i < length; i++ { + ch := rand.Int63n(charsetLen) + id[i] = idCharset[ch] + } + return string(id) +} diff --git a/game/main.go b/game/main.go new file mode 100644 index 0000000..1f5e367 --- /dev/null +++ b/game/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "zworld/engine" +) + +func main() { + engine.Run(engine.Args{ + Width: 1600, + Height: 1200, + Title: "zworld", + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ba67d72 --- /dev/null +++ b/go.mod @@ -0,0 +1,32 @@ +module zworld + +go 1.21.6 + +require ( + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240108052320-294b0144ba39 + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 + github.com/mitchellh/hashstructure/v2 v2.0.2 + github.com/ojrac/opensimplex-go v1.0.2 + github.com/onsi/ginkgo/v2 v2.14.0 + github.com/onsi/gomega v1.30.0 + github.com/qmuntal/gltf v0.24.2 + github.com/vkngwrapper/core/v2 v2.2.1 + github.com/vkngwrapper/extensions/v2 v2.2.0 + golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 + golang.org/x/image v0.15.0 +) + +require ( + github.com/CannibalVox/cgoparam v1.1.0 // indirect + github.com/go-logr/logr v1.3.0 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.17.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..31f7e7d --- /dev/null +++ b/go.sum @@ -0,0 +1,78 @@ +github.com/CannibalVox/cgoparam v1.1.0 h1:6UDDhOpT06csFE2vkcanXsIJmebMc9o+6Vzhvi4i0wY= +github.com/CannibalVox/cgoparam v1.1.0/go.mod h1:9LDFLuHVgE+IIBDd1QFN3dPqmGQN9bS6H+NPizMv2fA= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240108052320-294b0144ba39 h1:Fyfrfr+TP8w6ZQ2UXX2Slz8zMCPV/2d4WF78C39zGU0= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240108052320-294b0144ba39/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg= +github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/ojrac/opensimplex-go v1.0.2 h1:l4vs0D+JCakcu5OV0kJ99oEaWJfggSc9jiLpxaWvSzs= +github.com/ojrac/opensimplex-go v1.0.2/go.mod h1:NwbXFFbXcdGgIFdiA7/REME+7n/lOf1TuEbLiZYOWnM= +github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= +github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/qmuntal/gltf v0.24.2 h1:Cy9gabbcuWl/LJb3EIFXQIiWZ1Jf2V8ZygtiLc7Piyg= +github.com/qmuntal/gltf v0.24.2/go.mod h1:7FR0CRHoOehIgKTBVq/QVyvPn0i6tzp2AdIghb2bPg4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/vkngwrapper/core/v2 v2.2.1 h1:8xw2tuIXAeyNQj4mnDA7BHO6T6f7ba08UbJZB7UM6xg= +github.com/vkngwrapper/core/v2 v2.2.1/go.mod h1:EWABLJZGHa8nyeO4Bh9eR/V862HAz+Fvk5DitkOvYF4= +github.com/vkngwrapper/extensions/v2 v2.2.0 h1:2ZP+Nom2EbefqgR2EPherJRS836wSWPoXeOLvV7aUuY= +github.com/vkngwrapper/extensions/v2 v2.2.0/go.mod h1:55exjYwTUyQVNS/zhrC/Or/c2CA4Q9Cj/88Tu9EqlJ0= +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= +golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plugins/geometry/cone/cone.go b/plugins/geometry/cone/cone.go new file mode 100644 index 0000000..e7ce9e7 --- /dev/null +++ b/plugins/geometry/cone/cone.go @@ -0,0 +1,91 @@ +package cone + +import ( + "zworld/engine/object" + "zworld/engine/object/mesh" + "zworld/engine/renderapi/color" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/vertex" + "zworld/plugins/math" + "zworld/plugins/math/vec3" +) + +type Cone struct { + object.Object + *Mesh +} + +func NewObject(args Args) *Cone { + c := &Cone{ + Mesh: New(args), + } + c.Mesh.Name() + c.Object.Name() + return object.New("Cone", &Cone{ + Mesh: New(args), + }) +} + +// A Cone is a forward rendered colored cone mesh +type Mesh struct { + *mesh.Static + Args +} + +type Args struct { + Mat *material.Def + Radius float32 + Height float32 + Segments int + Color color.T +} + +func New(args Args) *Mesh { + if args.Mat == nil { + args.Mat = material.ColoredForward() + } + cone := object.NewComponent(&Mesh{ + Static: mesh.New(args.Mat), + Args: args, + }) + cone.generate() + return cone +} +func (c *Mesh) generate() { + data := make([]vertex.C, 6*c.Segments) + + // cone + top := vec3.New(0, c.Height, 0) + sangle := 2 * math.Pi / float32(c.Segments) + for i := 0; i < c.Segments; i++ { + a1 := sangle * (float32(i) + 0.5) + a2 := sangle * (float32(i) + 1.5) + v1 := vec3.New(math.Cos(a1), 0, -math.Sin(a1)).Scaled(c.Radius) + v2 := vec3.New(math.Cos(a2), 0, -math.Sin(a2)).Scaled(c.Radius) + v1t, v2t := top.Sub(v1), top.Sub(v2) + n := vec3.Cross(v1t, v2t).Normalized() + + o := 3 * i + data[o+0] = vertex.C{P: v2, N: n, C: c.Color.Vec4()} + data[o+1] = vertex.C{P: top, N: n, C: c.Color.Vec4()} + data[o+2] = vertex.C{P: v1, N: n, C: c.Color.Vec4()} + } + + // bottom + base := vec3.Zero + n := vec3.New(0, -1, 0) + for i := 0; i < c.Segments; i++ { + a1 := sangle * (float32(i) + 0.5) + a2 := sangle * (float32(i) + 1.5) + v1 := vec3.New(math.Cos(a1), 0, -math.Sin(a1)).Scaled(c.Radius) + v2 := vec3.New(math.Cos(a2), 0, -math.Sin(a2)).Scaled(c.Radius) + o := 3 * (i + c.Segments) + data[o+0] = vertex.C{P: v1, N: n, C: c.Color.Vec4()} + data[o+1] = vertex.C{P: base, N: n, C: c.Color.Vec4()} + data[o+2] = vertex.C{P: v2, N: n, C: c.Color.Vec4()} + } + + key := object.Key("cone", c) + mesh := vertex.NewTriangles(key, data, []uint16{}) + c.VertexData.Set(mesh) +} diff --git a/plugins/geometry/cube/cube.go b/plugins/geometry/cube/cube.go new file mode 100644 index 0000000..3273494 --- /dev/null +++ b/plugins/geometry/cube/cube.go @@ -0,0 +1,118 @@ +package cube + +import ( + "zworld/engine/object" + "zworld/engine/object/mesh" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/texture" + "zworld/engine/renderapi/vertex" + "zworld/plugins/math/vec2" + "zworld/plugins/math/vec3" +) + +type Object struct { + object.Object + *Mesh +} + +func NewObject(args Args) *Object { + return object.New("Cube", &Object{ + Mesh: New(args), + }) +} + +// Mesh is a vertex colored cube mesh +type Mesh struct { + *mesh.Static + Args +} + +type Args struct { + Mat *material.Def + Size float32 +} + +// New creates a vertex colored cube mesh with a given size +func New(args Args) *Mesh { + if args.Mat == nil { + args.Mat = material.ColoredForward() + } + cube := object.NewComponent(&Mesh{ + Static: mesh.New(args.Mat), + Args: args, + }) + cube.SetTexture(texture.Diffuse, texture.Checker) + cube.generate() + return cube +} + +func (c *Mesh) generate() { + s := c.Size / 2 + + topLeft := vec2.New(0, 0) + topRight := vec2.New(1, 0) + bottomLeft := vec2.New(0, 1) + bottomRight := vec2.New(1, 1) + + vertices := []vertex.T{ + // X+ + {P: vec3.New(s, -s, s), N: vec3.UnitX, T: bottomRight}, // 0 + {P: vec3.New(s, -s, -s), N: vec3.UnitX, T: bottomLeft}, // 1 + {P: vec3.New(s, s, -s), N: vec3.UnitX, T: topLeft}, // 2 + {P: vec3.New(s, s, s), N: vec3.UnitX, T: topRight}, // 3 + + // X- + {P: vec3.New(-s, -s, -s), N: vec3.UnitXN, T: bottomRight}, // 4 + {P: vec3.New(-s, -s, s), N: vec3.UnitXN, T: bottomLeft}, // 5 + {P: vec3.New(-s, s, s), N: vec3.UnitXN, T: topLeft}, // 6 + {P: vec3.New(-s, s, -s), N: vec3.UnitXN, T: topRight}, // 7 + + // Y+ + {P: vec3.New(s, s, -s), N: vec3.UnitY, T: bottomRight}, // 8 + {P: vec3.New(-s, s, -s), N: vec3.UnitY, T: bottomLeft}, // 9 + {P: vec3.New(-s, s, s), N: vec3.UnitY, T: topLeft}, // 10 + {P: vec3.New(s, s, s), N: vec3.UnitY, T: topRight}, // 11 + + // Y- + {P: vec3.New(-s, -s, -s), N: vec3.UnitYN, T: bottomRight}, // 12 + {P: vec3.New(s, -s, -s), N: vec3.UnitYN, T: bottomLeft}, // 13 + {P: vec3.New(s, -s, s), N: vec3.UnitYN, T: topLeft}, // 14 + {P: vec3.New(-s, -s, s), N: vec3.UnitYN, T: topRight}, // 15 + + // Z+ + {P: vec3.New(-s, -s, s), N: vec3.UnitZ, T: bottomRight}, // 16 + {P: vec3.New(s, -s, s), N: vec3.UnitZ, T: bottomLeft}, // 17 + {P: vec3.New(s, s, s), N: vec3.UnitZ, T: topLeft}, // 18 + {P: vec3.New(-s, s, s), N: vec3.UnitZ, T: topRight}, // 19 + + // Z- + {P: vec3.New(s, -s, -s), N: vec3.UnitZN, T: bottomRight}, // 20 + {P: vec3.New(-s, -s, -s), N: vec3.UnitZN, T: bottomLeft}, // 21 + {P: vec3.New(-s, s, -s), N: vec3.UnitZN, T: topLeft}, // 22 + {P: vec3.New(s, s, -s), N: vec3.UnitZN, T: topRight}, // 23 + } + + indices := []uint16{ + 0, 1, 2, + 0, 2, 3, + + 4, 5, 6, + 4, 6, 7, + + 8, 9, 10, + 8, 10, 11, + + 12, 13, 14, + 12, 14, 15, + + 16, 17, 18, + 16, 18, 19, + + 20, 21, 22, + 20, 22, 23, + } + + key := object.Key("cube", c) + mesh := vertex.NewTriangles(key, vertices, indices) + c.VertexData.Set(mesh) +} diff --git a/plugins/geometry/cylinder/cylinder.go b/plugins/geometry/cylinder/cylinder.go new file mode 100644 index 0000000..b5b3fe1 --- /dev/null +++ b/plugins/geometry/cylinder/cylinder.go @@ -0,0 +1,105 @@ +package cylinder + +import ( + "zworld/engine/object" + "zworld/engine/object/mesh" + "zworld/engine/renderapi/color" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/vertex" + "zworld/plugins/math" + "zworld/plugins/math/vec3" +) + +type Cylinder struct { + object.Object + Mesh *Mesh +} + +func NewObject(args Args) *Cylinder { + return object.New("Cylinder", &Cylinder{ + Mesh: New(args), + }) +} + +// A Cylinder is a forward rendered colored cyllinder mesh +type Mesh struct { + *mesh.Static + Args +} + +type Args struct { + Mat *material.Def + Radius float32 + Height float32 + Segments int + Color color.T +} + +func New(args Args) *Mesh { + if args.Mat == nil { + args.Mat = material.ColoredForward() + } + cyllinder := object.NewComponent(&Mesh{ + Static: mesh.New(args.Mat), + Args: args, + }) + // this should not run on the main thread + cyllinder.generate() + return cyllinder +} + +func (c *Mesh) generate() { + // vertex order: clockwise + + data := make([]vertex.C, 2*2*3*c.Segments) + hh := c.Height / 2 + sangle := 2 * math.Pi / float32(c.Segments) + color := c.Color.Vec4() + + // top + top := vec3.New(0, hh, 0) + bottom := vec3.New(0, -hh, 0) + for i := 0; i < c.Segments; i++ { + o := 12 * i // segment vertex offset + + right := sangle * (float32(i) + 0.5) + left := sangle * (float32(i) + 1.5) + topRight := vec3.New(math.Cos(right), 0, -math.Sin(right)).Scaled(c.Radius) + topRight.Y = hh + topLeft := vec3.New(math.Cos(left), 0, -math.Sin(left)).Scaled(c.Radius) + topLeft.Y = hh + bottomRight := vec3.New(math.Cos(right), 0, -math.Sin(right)).Scaled(c.Radius) + bottomRight.Y = -hh + bottomLeft := vec3.New(math.Cos(left), 0, -math.Sin(left)).Scaled(c.Radius) + bottomLeft.Y = -hh + + // top face + data[o+0] = vertex.C{P: topLeft, N: vec3.Up, C: color} + data[o+1] = vertex.C{P: top, N: vec3.Up, C: color} + data[o+2] = vertex.C{P: topRight, N: vec3.Up, C: color} + + // bottom face + data[o+3] = vertex.C{P: bottomRight, N: vec3.Down, C: color} + data[o+4] = vertex.C{P: bottom, N: vec3.Down, C: color} + data[o+5] = vertex.C{P: bottomLeft, N: vec3.Down, C: color} + + // calculate segment normal + nv1 := topRight.Sub(bottomLeft) + nv2 := bottomRight.Sub(bottomLeft) + n := vec3.Cross(nv1, nv2) + + // side face 1 + data[o+6] = vertex.C{P: topRight, N: n, C: color} + data[o+7] = vertex.C{P: bottomLeft, N: n, C: color} + data[o+8] = vertex.C{P: topLeft, N: n, C: color} + + // side face 2 + data[o+9] = vertex.C{P: bottomRight, N: n, C: color} + data[o+10] = vertex.C{P: bottomLeft, N: n, C: color} + data[o+11] = vertex.C{P: topRight, N: n, C: color} + } + + key := object.Key("cylinder", c) + mesh := vertex.NewTriangles(key, data, []uint16{}) + c.VertexData.Set(mesh) +} diff --git a/plugins/geometry/gltf/asset.go b/plugins/geometry/gltf/asset.go new file mode 100644 index 0000000..38debe1 --- /dev/null +++ b/plugins/geometry/gltf/asset.go @@ -0,0 +1,204 @@ +package gltf + +import ( + "fmt" + "github.com/qmuntal/gltf" + "strings" + "zworld/engine/object" + "zworld/engine/object/mesh" + "zworld/engine/renderapi/types" + "zworld/engine/renderapi/vertex" + "zworld/plugins/math/quat" + "zworld/plugins/math/vec3" +) + +func Load(path string) object.Component { + assetPath := fmt.Sprintf("assets/%s", path) + doc, _ := gltf.Open(assetPath) + + // load default scene + scene := doc.Scenes[*doc.Scene] + + return loadScene(doc, scene) +} + +func loadScene(doc *gltf.Document, scene *gltf.Scene) object.Component { + root := object.Empty(scene.Name) + + for _, nodeId := range scene.Nodes { + node := loadNode(doc, doc.Nodes[nodeId]) + object.Attach(root, node) + } + + // rotate to get Y+ up + root.Transform().SetRotation(quat.Euler(90, 0, 0)) + + return root +} + +func loadNode(doc *gltf.Document, node *gltf.Node) object.Component { + obj := object.Empty(node.Name) + + // mesh components + if node.Mesh != nil { + msh := doc.Meshes[*node.Mesh] + for _, primitive := range msh.Primitives { + renderer := loadPrimitive(doc, msh.Name, primitive) + object.Attach(obj, renderer) + } + } + + // object transform + obj.Transform().SetPosition(vec3.FromSlice(node.Translation[:3])) + obj.Transform().SetRotation(quat.T{W: node.Rotation[3], V: vec3.FromSlice(node.Rotation[:3])}) + obj.Transform().SetScale(vec3.FromSlice(node.Scale[:3])) + + // child objects + for _, child := range node.Children { + object.Attach(obj, loadNode(doc, doc.Nodes[child])) + } + + return obj +} + +func loadPrimitive(doc *gltf.Document, name string, primitive *gltf.Primitive) mesh.Mesh { + kind := mapPrimitiveType(primitive.Mode) + + // create interleaved buffers + pointers, vertexData := createBuffer(doc, primitive) + indexElements, indexData := createIndexBuffer(doc, primitive) + + // ensure vertex attribute names are in lowercase + for i, ptr := range pointers { + pointers[i].Name = strings.ToLower(ptr.Name) + } + + // mesh data + gmesh := &gltfMesh{ + key: name, + primitive: kind, + elements: indexElements, + pointers: pointers, + vertices: vertexData, + indices: indexData, + indexsize: len(indexData) / indexElements, + } + + // create mesh component + mesh := mesh.NewPrimitiveMesh(kind, nil) + mesh.VertexData.Set(gmesh) + return mesh +} + +func extractPointers(doc *gltf.Document, primitive *gltf.Primitive) []vertex.Pointer { + offset := 0 + pointers := make(vertex.Pointers, 0, len(primitive.Attributes)) + for name, index := range primitive.Attributes { + accessor := doc.Accessors[index] + + pointers = append(pointers, vertex.Pointer{ + Name: name, + Source: mapComponentType(accessor.ComponentType), + Offset: offset, + Elements: int(accessor.Type.Components()), + Normalize: accessor.Normalized, + Stride: 0, // filed in in next pass + }) + + size := int(accessor.ComponentType.ByteSize() * accessor.Type.Components()) + offset += size + } + + // at this point, offset equals the final stride value. fill it in + for index := range pointers { + pointers[index].Stride = offset + } + + return pointers +} + +func createBuffer(doc *gltf.Document, primitive *gltf.Primitive) (vertex.Pointers, []byte) { + pointers := extractPointers(doc, primitive) + + count := int(doc.Accessors[primitive.Attributes[pointers[0].Name]].Count) + size := count * pointers[0].Stride + + output := make([]byte, size) + + for _, ptr := range pointers { + accessor := doc.Accessors[primitive.Attributes[ptr.Name]] + view := doc.BufferViews[*accessor.BufferView] + buffer := doc.Buffers[view.Buffer] + size := int(accessor.ComponentType.ByteSize() * accessor.Type.Components()) + stride := size + if view.ByteStride != 0 { + stride = int(view.ByteStride) + } + + for i := 0; i < count; i++ { + srcStart := int(view.ByteOffset) + i*stride + int(accessor.ByteOffset) + srcEnd := srcStart + size + dstStart := i*ptr.Stride + ptr.Offset + dstEnd := dstStart + size + + copy(output[dstStart:dstEnd], buffer.Data[srcStart:srcEnd]) + } + } + + return pointers, output +} + +func createIndexBuffer(doc *gltf.Document, primitive *gltf.Primitive) (int, []byte) { + accessor := doc.Accessors[*primitive.Indices] + view := doc.BufferViews[*accessor.BufferView] + buffer := doc.Buffers[view.Buffer] + + count := int(accessor.Count) + size := int(accessor.ComponentType.ByteSize() * accessor.Type.Components()) + stride := size + if view.ByteStride != 0 { + stride = int(view.ByteStride) + } + + output := make([]byte, size*count) + for i := 0; i < count; i++ { + srcStart := int(view.ByteOffset) + i*stride + int(accessor.ByteOffset) + srcEnd := srcStart + size + dstStart := i * size + dstEnd := dstStart + size + + copy(output[dstStart:dstEnd], buffer.Data[srcStart:srcEnd]) + } + + return count, output +} + +func mapPrimitiveType(mode gltf.PrimitiveMode) vertex.Primitive { + switch mode { + case gltf.PrimitiveTriangles: + return vertex.Triangles + case gltf.PrimitiveLines: + return vertex.Lines + default: + panic("unsupported render primitive") + } +} + +func mapComponentType(kind gltf.ComponentType) types.Type { + switch kind { + case gltf.ComponentFloat: + return types.Float + case gltf.ComponentByte: + return types.Int8 + case gltf.ComponentUbyte: + return types.UInt8 + case gltf.ComponentShort: + return types.Int16 + case gltf.ComponentUshort: + return types.UInt16 + case gltf.ComponentUint: + return types.UInt32 + default: + panic(fmt.Sprintf("unmapped type %s (%d)", kind, kind)) + } +} diff --git a/plugins/geometry/gltf/mesh.go b/plugins/geometry/gltf/mesh.go new file mode 100644 index 0000000..3b4c8cf --- /dev/null +++ b/plugins/geometry/gltf/mesh.go @@ -0,0 +1,37 @@ +package gltf + +import ( + "zworld/engine/renderapi/vertex" + "zworld/plugins/math/vec3" +) + +type gltfMesh struct { + key string + elements int + primitive vertex.Primitive + pointers vertex.Pointers + indices []byte + vertices []byte + indexsize int +} + +var _ vertex.Mesh = &gltfMesh{} + +func (m *gltfMesh) Key() string { return m.key } +func (m *gltfMesh) Version() int { return 1 } +func (m *gltfMesh) IndexCount() int { return m.elements } +func (m *gltfMesh) IndexSize() int { return m.indexsize } +func (m *gltfMesh) IndexData() any { return m.indices } +func (m *gltfMesh) VertexCount() int { return len(m.vertices) / m.VertexSize() } +func (m *gltfMesh) VertexSize() int { return m.pointers.Stride() } +func (m *gltfMesh) VertexData() any { return m.vertices } + +func (m *gltfMesh) Positions(func(vec3.T)) { panic("not implemented") } + +func (m *gltfMesh) Triangles(func(vertex.Triangle)) { panic("not implemented") } + +func (m *gltfMesh) Min() vec3.T { panic("not implemented") } +func (m *gltfMesh) Max() vec3.T { panic("not implemented") } + +func (m *gltfMesh) Primitive() vertex.Primitive { return m.primitive } +func (m *gltfMesh) Pointers() vertex.Pointers { return m.pointers } diff --git a/plugins/geometry/lines/box.go b/plugins/geometry/lines/box.go new file mode 100644 index 0000000..090c370 --- /dev/null +++ b/plugins/geometry/lines/box.go @@ -0,0 +1,91 @@ +package lines + +import ( + "zworld/engine/object" + "zworld/engine/object/mesh" + "zworld/engine/renderapi/color" + "zworld/engine/renderapi/vertex" + "zworld/plugins/math/vec3" +) + +type BoxObject struct { + object.Object + *Box +} + +func NewBoxObject(args BoxArgs) *BoxObject { + return object.New("Box", &BoxObject{ + Box: NewBox(args), + }) +} + +type Box struct { + *mesh.Static + Extents object.Property[vec3.T] + Color object.Property[color.T] + + data vertex.MutableMesh[vertex.C, uint16] +} + +// Args are kinda like props +// If they change, we should recomupte the mesh + +type BoxArgs struct { + Extents vec3.T + Color color.T +} + +func NewBox(args BoxArgs) *Box { + b := object.NewComponent(&Box{ + Static: mesh.NewLines(), + Extents: object.NewProperty(args.Extents), + Color: object.NewProperty(args.Color), + }) + b.data = vertex.NewLines[vertex.C, uint16](object.Key("box", b), nil, nil) + b.Extents.OnChange.Subscribe(func(vec3.T) { b.refresh() }) + b.Color.OnChange.Subscribe(func(color.T) { b.refresh() }) + b.refresh() + return b +} + +func (b *Box) refresh() { + halfsize := b.Extents.Get().Scaled(0.5) + w, h, d := halfsize.X, halfsize.Y, halfsize.Z + c := b.Color.Get().Vec4() + + vertices := []vertex.C{ + // bottom square + {P: vec3.New(-w, -h, -d), C: c}, // 0 + {P: vec3.New(+w, -h, -d), C: c}, // 1 + {P: vec3.New(-w, -h, +d), C: c}, // 2 + {P: vec3.New(+w, -h, +d), C: c}, // 3 + + // top square + {P: vec3.New(-w, +h, -d), C: c}, // 4 + {P: vec3.New(+w, +h, -d), C: c}, // 5 + {P: vec3.New(-w, +h, +d), C: c}, // 6 + {P: vec3.New(+w, +h, +d), C: c}, // 7 + } + indices := []uint16{ + // bottom + 0, 1, + 0, 2, + 1, 3, + 2, 3, + + // top + 4, 5, + 4, 6, + 5, 7, + 6, 7, + + // sides + 0, 4, + 1, 5, + 2, 6, + 3, 7, + } + + b.data.Update(vertices, indices) + b.VertexData.Set(b.data) +} diff --git a/plugins/geometry/lines/immediate.go b/plugins/geometry/lines/immediate.go new file mode 100644 index 0000000..1f3f5a7 --- /dev/null +++ b/plugins/geometry/lines/immediate.go @@ -0,0 +1,50 @@ +package lines + +import ( + "zworld/engine/object/mesh" + "zworld/engine/renderapi/color" + "zworld/plugins/math/vec3" +) + +var Debug = &DebugLines{} + +type DebugLines struct { + enabled bool + frame int + meshes []*Lines +} + +func (li *DebugLines) Setup(frames int) { + li.meshes = make([]*Lines, frames) + for i := range li.meshes { + li.meshes[i] = New(Args{}) + } + li.enabled = true +} + +func (li *DebugLines) Add(from, to vec3.T, clr color.T) { + if !li.enabled { + return + } + mesh := li.meshes[li.frame] + mesh.Lines = append(mesh.Lines, Line{ + Start: from, + End: to, + Color: clr, + }) +} + +func (li *DebugLines) Fetch() mesh.Mesh { + // build mesh for current frame + mesh := li.meshes[li.frame] + mesh.Refresh() + + // set next frame + li.frame = (li.frame + 1) % len(li.meshes) + + // prepare next mesh + nextMesh := li.meshes[li.frame] + nextMesh.Clear() + + return mesh +} diff --git a/plugins/geometry/lines/line.go b/plugins/geometry/lines/line.go new file mode 100644 index 0000000..c8c7231 --- /dev/null +++ b/plugins/geometry/lines/line.go @@ -0,0 +1,17 @@ +package lines + +import ( + "zworld/engine/renderapi/color" + "zworld/plugins/math/vec3" +) + +type Line struct { + Start vec3.T + End vec3.T + Color color.T +} + +// L creates a new line segment +func L(start, end vec3.T, color color.T) Line { + return Line{start, end, color} +} diff --git a/plugins/geometry/lines/lines.go b/plugins/geometry/lines/lines.go new file mode 100644 index 0000000..c78557d --- /dev/null +++ b/plugins/geometry/lines/lines.go @@ -0,0 +1,62 @@ +package lines + +import ( + "zworld/engine/object" + "zworld/engine/object/mesh" + "zworld/engine/renderapi/color" + "zworld/engine/renderapi/vertex" + "zworld/plugins/math/vec3" +) + +type Lines struct { + *mesh.Static + Args +} + +type Args struct { + Lines []Line + + lineMesh vertex.MutableMesh[vertex.C, uint16] +} + +func New(args Args) *Lines { + b := object.NewComponent(&Lines{ + Static: mesh.NewLines(), + Args: args, + }) + b.lineMesh = vertex.NewLines(object.Key("lines", b), []vertex.C{}, []uint16{}) + b.VertexData.Set(b.lineMesh) + b.Refresh() + return b +} + +func (li *Lines) Add(from, to vec3.T, clr color.T) { + li.Lines = append(li.Lines, Line{ + Start: from, + End: to, + Color: clr, + }) +} + +func (li *Lines) Clear() { + li.Lines = li.Lines[:0] +} + +func (li *Lines) Count() int { + return len(li.Lines) +} + +func (li *Lines) Refresh() { + count := len(li.Lines) + vertices := make([]vertex.C, 2*count) + for i := 0; i < count; i++ { + line := li.Lines[i] + a := &vertices[2*i+0] + b := &vertices[2*i+1] + a.P = line.Start + a.C = line.Color.Vec4() + b.P = line.End + b.C = line.Color.Vec4() + } + li.lineMesh.Update(vertices, []uint16{}) +} diff --git a/plugins/geometry/lines/sphere.go b/plugins/geometry/lines/sphere.go new file mode 100644 index 0000000..e20eefc --- /dev/null +++ b/plugins/geometry/lines/sphere.go @@ -0,0 +1,101 @@ +package lines + +import ( + "zworld/engine/object" + "zworld/engine/object/mesh" + "zworld/engine/renderapi/color" + "zworld/engine/renderapi/vertex" + "zworld/plugins/math" + "zworld/plugins/math/vec3" +) + +type Sphere struct { + *mesh.Static + Radius object.Property[float32] + Color object.Property[color.T] + + data vertex.MutableMesh[vertex.C, uint16] + xcolor color.T + ycolor color.T + zcolor color.T +} + +type SphereArgs struct { + Radius float32 + Color color.T +} + +func NewSphere(args SphereArgs) *Sphere { + b := object.NewComponent(&Sphere{ + Static: mesh.NewLines(), + Radius: object.NewProperty(args.Radius), + Color: object.NewProperty(args.Color), + }) + b.Radius.OnChange.Subscribe(func(float32) { b.refresh() }) + b.Color.OnChange.Subscribe(func(c color.T) { + b.SetAxisColors(c, c, c) + b.refresh() + }) + b.data = vertex.NewLines[vertex.C, uint16](object.Key("sphere", b), nil, nil) + b.SetAxisColors(args.Color, args.Color, args.Color) + return b +} + +func (b *Sphere) SetAxisColors(x color.T, y color.T, z color.T) { + b.xcolor = x + b.ycolor = y + b.zcolor = z + b.refresh() +} + +func (b *Sphere) refresh() { + segments := 32 + radius := b.Radius.Get() + angle := 2 * math.Pi / float32(segments) + vertices := make([]vertex.C, 0, 2*3*segments) + + // x ring + for i := 0; i < segments; i++ { + a0 := float32(i) * angle + a1 := float32(i+1) * angle + vertices = append(vertices, vertex.C{ + P: vec3.New(0, math.Sin(a0), math.Cos(a0)).Scaled(radius), + C: b.xcolor.Vec4(), + }) + vertices = append(vertices, vertex.C{ + P: vec3.New(0, math.Sin(a1), math.Cos(a1)).Scaled(radius), + C: b.xcolor.Vec4(), + }) + } + + // y ring + for i := 0; i < segments; i++ { + a0 := float32(i) * angle + a1 := float32(i+1) * angle + vertices = append(vertices, vertex.C{ + P: vec3.New(math.Cos(a0), 0, math.Sin(a0)).Scaled(radius), + C: b.ycolor.Vec4(), + }) + vertices = append(vertices, vertex.C{ + P: vec3.New(math.Cos(a1), 0, math.Sin(a1)).Scaled(radius), + C: b.ycolor.Vec4(), + }) + } + + // z ring + for i := 0; i < segments; i++ { + a0 := float32(i) * angle + a1 := float32(i+1) * angle + vertices = append(vertices, vertex.C{ + P: vec3.New(math.Cos(a0), math.Sin(a0), 0).Scaled(radius), + C: b.zcolor.Vec4(), + }) + vertices = append(vertices, vertex.C{ + P: vec3.New(math.Cos(a1), math.Sin(a1), 0).Scaled(radius), + C: b.zcolor.Vec4(), + }) + } + + b.data.Update(vertices, nil) + b.VertexData.Set(b.data) +} diff --git a/plugins/geometry/lines/wireframe.go b/plugins/geometry/lines/wireframe.go new file mode 100644 index 0000000..5d13ccc --- /dev/null +++ b/plugins/geometry/lines/wireframe.go @@ -0,0 +1,53 @@ +package lines + +import ( + "zworld/engine/object" + "zworld/engine/object/mesh" + "zworld/engine/renderapi/color" + "zworld/engine/renderapi/vertex" +) + +type Wireframe struct { + *mesh.Static + Color object.Property[color.T] + Source object.Property[vertex.Mesh] + + data vertex.MutableMesh[vertex.C, uint32] + offset float32 +} + +func NewWireframe(msh vertex.Mesh, clr color.T) *Wireframe { + w := object.NewComponent(&Wireframe{ + Static: mesh.NewLines(), + Color: object.NewProperty(clr), + Source: object.NewProperty(msh), + }) + w.Color.OnChange.Subscribe(func(color.T) { w.refresh() }) + w.Source.OnChange.Subscribe(func(vertex.Mesh) { w.refresh() }) + w.data = vertex.NewLines[vertex.C, uint32](object.Key("wireframe", w), nil, nil) + w.refresh() + return w +} + +func (w *Wireframe) refresh() { + clr := w.Color.Get().Vec4() + msh := w.Source.Get() + + indices := make([]uint32, 0, msh.IndexCount()*2) + vertices := make([]vertex.C, 0, msh.VertexCount()) + + msh.Triangles(func(t vertex.Triangle) { + index := uint32(len(vertices)) + offset := t.Normal().Scaled(w.offset) + vertices = append(vertices, vertex.C{P: t.A.Add(offset), C: clr}) // +0 + vertices = append(vertices, vertex.C{P: t.B.Add(offset), C: clr}) // +1 + vertices = append(vertices, vertex.C{P: t.C.Add(offset), C: clr}) // +2 + + indices = append(indices, index+0, index+1) // A-B + indices = append(indices, index+1, index+2) // B-C + indices = append(indices, index+2, index+0) // C-A + }) + + w.data.Update(vertices, indices) + w.VertexData.Set(w.data) +} diff --git a/plugins/geometry/plane/plane.go b/plugins/geometry/plane/plane.go new file mode 100644 index 0000000..a26d7fb --- /dev/null +++ b/plugins/geometry/plane/plane.go @@ -0,0 +1,73 @@ +package plane + +import ( + "zworld/engine/object" + "zworld/engine/object/mesh" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/vertex" + "zworld/plugins/math/vec2" + "zworld/plugins/math/vec3" +) + +type Plane struct { + object.Object + *Mesh +} + +func NewObject(args Args) *Plane { + return object.New("Plane", &Plane{ + Mesh: New(args), + }) +} + +// Plane is a single segment, two-sided 3D plane +type Mesh struct { + *mesh.Static + Size object.Property[vec2.T] + + data vertex.MutableMesh[vertex.T, uint16] +} + +type Args struct { + Size vec2.T + Mat *material.Def +} + +func New(args Args) *Mesh { + if args.Mat == nil { + args.Mat = material.StandardForward() + } + p := object.NewComponent(&Mesh{ + Static: mesh.New(args.Mat), + Size: object.NewProperty[vec2.T](args.Size), + }) + p.data = vertex.NewTriangles[vertex.T, uint16](object.Key("plane", p), nil, nil) + p.Size.OnChange.Subscribe(func(f vec2.T) { p.refresh() }) + p.refresh() + return p +} + +func (p *Mesh) refresh() { + s := p.Size.Get().Scaled(0.5) + y := float32(0.001) + + vertices := []vertex.T{ + {P: vec3.New(-s.X, y, -s.Y), N: vec3.UnitY, T: vec2.New(0, 1)}, // o1 + {P: vec3.New(s.X, y, -s.Y), N: vec3.UnitY, T: vec2.New(1, 1)}, // x1 + {P: vec3.New(-s.X, y, s.Y), N: vec3.UnitY, T: vec2.New(0, 0)}, // z1 + {P: vec3.New(s.X, y, s.Y), N: vec3.UnitY, T: vec2.New(1, 0)}, // d1 + + {P: vec3.New(-s.X, -y, -s.Y), N: vec3.UnitYN, T: vec2.New(0, 0)}, // o2 + {P: vec3.New(s.X, -y, -s.Y), N: vec3.UnitYN, T: vec2.New(0, 0)}, // x2 + {P: vec3.New(-s.X, -y, s.Y), N: vec3.UnitYN, T: vec2.New(0, 0)}, // z2 + {P: vec3.New(s.X, -y, s.Y), N: vec3.UnitYN, T: vec2.New(0, 0)}, // d2 + } + + indices := []uint16{ + 0, 2, 1, 1, 2, 3, // top + 5, 6, 4, 7, 6, 5, // bottom + } + + p.data.Update(vertices, indices) + p.VertexData.Set(p.data) +} diff --git a/plugins/geometry/sphere/sphere.go b/plugins/geometry/sphere/sphere.go new file mode 100644 index 0000000..4d82b1f --- /dev/null +++ b/plugins/geometry/sphere/sphere.go @@ -0,0 +1,115 @@ +package sphere + +import ( + "zworld/engine/object" + "zworld/engine/object/mesh" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/texture" + "zworld/engine/renderapi/vertex" + "zworld/plugins/math/vec2" + "zworld/plugins/math/vec3" +) + +type Mesh struct { + *mesh.Static + Subdivisions object.Property[int] + + data vertex.MutableMesh[vertex.T, uint16] +} + +func New(mat *material.Def) *Mesh { + m := object.NewComponent(&Mesh{ + Static: mesh.New(mat), + Subdivisions: object.NewProperty(3), + }) + m.SetTexture(texture.Diffuse, texture.Checker) + m.data = vertex.NewTriangles[vertex.T, uint16](object.Key("sphere", m), nil, nil) + m.Subdivisions.OnChange.Subscribe(func(int) { m.refresh() }) + m.refresh() + return m +} + +func (m *Mesh) refresh() { + tris := icosphere(m.Subdivisions.Get()) + + vertices := []vertex.T{} + for _, tri := range tris { + vertices = append(vertices, vertex.T{ + P: tri.A, + N: tri.A, + T: vec2.New(0, 0), + }) + vertices = append(vertices, vertex.T{ + P: tri.B, + N: tri.B, + T: vec2.New(0, 0), + }) + vertices = append(vertices, vertex.T{ + P: tri.C, + N: tri.C, + T: vec2.New(0, 0), + }) + } + + m.data.Update(vertices, nil) + m.VertexData.Set(m.data) +} + +func icosphere(subdivisions int) []vertex.Triangle { + const X = float32(0.525731112119133606) + const Z = float32(0.850650808352039932) + + vertices := []vec3.T{ + vec3.New(-X, 0, Z), + vec3.New(X, 0, Z), + vec3.New(-X, 0, -Z), + vec3.New(X, 0, -Z), + vec3.New(0, Z, X), + vec3.New(0, Z, -X), + vec3.New(0, -Z, X), + vec3.New(0, -Z, -X), + vec3.New(Z, X, 0), + vec3.New(-Z, X, 0), + vec3.New(Z, -X, 0), + vec3.New(-Z, -X, 0), + } + + faces := []vertex.Triangle{ + {A: vertices[1], B: vertices[4], C: vertices[0]}, + {A: vertices[4], B: vertices[9], C: vertices[0]}, + {A: vertices[4], B: vertices[5], C: vertices[9]}, + {A: vertices[8], B: vertices[5], C: vertices[4]}, + {A: vertices[1], B: vertices[8], C: vertices[4]}, + {A: vertices[1], B: vertices[10], C: vertices[8]}, + {A: vertices[10], B: vertices[3], C: vertices[8]}, + {A: vertices[8], B: vertices[3], C: vertices[5]}, + {A: vertices[3], B: vertices[2], C: vertices[5]}, + {A: vertices[3], B: vertices[7], C: vertices[2]}, + {A: vertices[3], B: vertices[10], C: vertices[7]}, + {A: vertices[10], B: vertices[6], C: vertices[7]}, + {A: vertices[6], B: vertices[11], C: vertices[7]}, + {A: vertices[6], B: vertices[0], C: vertices[11]}, + {A: vertices[6], B: vertices[1], C: vertices[0]}, + {A: vertices[10], B: vertices[1], C: vertices[6]}, + {A: vertices[11], B: vertices[0], C: vertices[9]}, + {A: vertices[2], B: vertices[11], C: vertices[9]}, + {A: vertices[5], B: vertices[2], C: vertices[9]}, + {A: vertices[11], B: vertices[2], C: vertices[7]}, + } + + for r := subdivisions; r > 0; r-- { + result := make([]vertex.Triangle, 0, 4*len(faces)) + for _, tri := range faces { + v1 := vec3.Mid(tri.A, tri.B).Normalized() + v2 := vec3.Mid(tri.B, tri.C).Normalized() + v3 := vec3.Mid(tri.C, tri.A).Normalized() + result = append(result, vertex.Triangle{A: tri.A, B: v1, C: v3}) + result = append(result, vertex.Triangle{A: tri.B, B: v2, C: v1}) + result = append(result, vertex.Triangle{A: tri.C, B: v3, C: v2}) + result = append(result, vertex.Triangle{A: v1, B: v2, C: v3}) + } + faces = result + } + + return faces +} diff --git a/plugins/geometry/sprite/material.go b/plugins/geometry/sprite/material.go new file mode 100644 index 0000000..a45363b --- /dev/null +++ b/plugins/geometry/sprite/material.go @@ -0,0 +1,21 @@ +package sprite + +import ( + "github.com/vkngwrapper/core/v2/core1_0" + "zworld/engine/renderapi/material" + "zworld/engine/renderapi/vertex" +) + +func Material() *material.Def { + return &material.Def{ + Pass: material.Forward, + Shader: "forward/sprite", + VertexFormat: vertex.T{}, + DepthTest: true, + DepthWrite: true, + DepthFunc: core1_0.CompareOpLessOrEqual, + Primitive: vertex.Triangles, + CullMode: vertex.CullNone, + Transparent: true, + } +} diff --git a/plugins/geometry/sprite/sprite.go b/plugins/geometry/sprite/sprite.go new file mode 100644 index 0000000..a57fa71 --- /dev/null +++ b/plugins/geometry/sprite/sprite.go @@ -0,0 +1,71 @@ +package sprite + +import ( + "zworld/engine/object" + "zworld/engine/object/mesh" + "zworld/engine/renderapi/texture" + "zworld/engine/renderapi/vertex" + "zworld/plugins/math/vec2" + "zworld/plugins/math/vec3" +) + +type Sprite struct { + object.Object + *Mesh +} + +func NewObject(args Args) *Sprite { + return object.New("Sprite", &Sprite{ + Mesh: New(args), + }) +} + +// Sprite is a single segment, two-sided 3D plane +type Mesh struct { + *mesh.Static + Size object.Property[vec2.T] + Sprite object.Property[texture.Ref] + + mesh vertex.MutableMesh[vertex.T, uint16] +} + +var _ mesh.Mesh = &Mesh{} + +type Args struct { + Size vec2.T + Texture texture.Ref +} + +func New(args Args) *Mesh { + sprite := object.NewComponent(&Mesh{ + Static: mesh.New(Material()), + Size: object.NewProperty(args.Size), + Sprite: object.NewProperty(args.Texture), + }) + + sprite.mesh = vertex.NewTriangles[vertex.T, uint16]("sprite", nil, nil) + sprite.generate() + + sprite.SetTexture(texture.Diffuse, args.Texture) + sprite.Sprite.OnChange.Subscribe(func(tex texture.Ref) { + sprite.SetTexture(texture.Diffuse, tex) + }) + + return sprite +} + +func (p *Mesh) generate() { + w, h := p.Size.Get().X, p.Size.Get().Y + vertices := []vertex.T{ + {P: vec3.New(-0.5*w, -0.5*h, 0), T: vec2.New(0, 1)}, + {P: vec3.New(0.5*w, 0.5*h, 0), T: vec2.New(1, 0)}, + {P: vec3.New(-0.5*w, 0.5*h, 0), T: vec2.New(0, 0)}, + {P: vec3.New(0.5*w, -0.5*h, 0), T: vec2.New(1, 1)}, + } + indices := []uint16{ + 0, 1, 2, + 0, 3, 1, + } + p.mesh.Update(vertices, indices) + p.VertexData.Set(p.mesh) +} diff --git a/plugins/math/byte4/byte4.go b/plugins/math/byte4/byte4.go new file mode 100644 index 0000000..cf0c74a --- /dev/null +++ b/plugins/math/byte4/byte4.go @@ -0,0 +1,10 @@ +package byte4 + +// T is a 4-component vector of uint8 (bytes) +type T struct { + X, Y, Z, W byte +} + +func New(x, y, z, w byte) T { + return T{x, y, z, w} +} diff --git a/plugins/math/ivec2/ivec2.go b/plugins/math/ivec2/ivec2.go new file mode 100644 index 0000000..20a3ec7 --- /dev/null +++ b/plugins/math/ivec2/ivec2.go @@ -0,0 +1,32 @@ +package ivec2 + +var Zero = T{} +var One = T{X: 1, Y: 1} +var UnitX = T{X: 1} +var UnitY = T{Y: 1} + +type T struct { + X int + Y int +} + +func New(x, y int) T { + return T{ + X: x, + Y: y, + } +} + +func (v T) Add(v2 T) T { + return T{ + X: v.X + v2.X, + Y: v.Y + v2.Y, + } +} + +func (v T) Sub(v2 T) T { + return T{ + X: v.X - v2.X, + Y: v.Y - v2.Y, + } +} diff --git a/plugins/math/mat4/mat4.go b/plugins/math/mat4/mat4.go new file mode 100644 index 0000000..9d59b78 --- /dev/null +++ b/plugins/math/mat4/mat4.go @@ -0,0 +1,317 @@ +// Based on code from github.com/go-gl/mathgl: +// Copyright 2014 The go-gl Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +package mat4 + +import ( + "bytes" + "fmt" + "text/tabwriter" + + "golang.org/x/image/math/f32" + + "zworld/plugins/math" + "zworld/plugins/math/vec3" + "zworld/plugins/math/vec4" +) + +// T holds a 4x4 float32 matrix +type T f32.Mat4 + +// Add performs an element-wise addition of two matrices, this is +// equivalent to iterating over every element of m and adding the corresponding value of m2. +func (m *T) Add(m2 *T) T { + return T{ + m[0] + m2[0], m[1] + m2[1], m[2] + m2[2], m[3] + m2[3], + m[4] + m2[4], m[5] + m2[5], m[6] + m2[6], m[7] + m2[7], + m[8] + m2[8], m[9] + m2[9], m[10] + m2[10], m[11] + m2[11], + m[12] + m2[12], m[13] + m2[13], m[14] + m2[14], m[15] + m2[15], + } +} + +// Sub performs an element-wise subtraction of two matrices, this is +// equivalent to iterating over every element of m and subtracting the corresponding value of m2. +func (m *T) Sub(m2 *T) T { + return T{ + m[0] - m2[0], m[1] - m2[1], m[2] - m2[2], m[3] - m2[3], + m[4] - m2[4], m[5] - m2[5], m[6] - m2[6], m[7] - m2[7], + m[8] - m2[8], m[9] - m2[9], m[10] - m2[10], m[11] - m2[11], + m[12] - m2[12], m[13] - m2[13], m[14] - m2[14], m[15] - m2[15], + } +} + +// Scale performs a scalar multiplcation of the matrix. This is equivalent to iterating +// over every element of the matrix and multiply it by c. +func (m T) Scale(c float32) T { + return T{ + m[0] * c, m[1] * c, m[2] * c, m[3] * c, + m[4] * c, m[5] * c, m[6] * c, m[7] * c, + m[8] * c, m[9] * c, m[10] * c, m[11] * c, + m[12] * c, m[13] * c, m[14] * c, m[15] * c, + } +} + +// VMul multiplies a vec4 with the matrix +func (m *T) VMul(v vec4.T) vec4.T { + return vec4.T{ + X: m[0]*v.X + m[4]*v.Y + m[8]*v.Z + m[12]*v.W, + Y: m[1]*v.X + m[5]*v.Y + m[9]*v.Z + m[13]*v.W, + Z: m[2]*v.X + m[6]*v.Y + m[10]*v.Z + m[14]*v.W, + W: m[3]*v.X + m[7]*v.Y + m[11]*v.Z + m[15]*v.W, + } +} + +// TransformPoint transforms a point to world space +func (m *T) TransformPoint(v vec3.T) vec3.T { + p := vec4.Extend(v, 1) + vt := m.VMul(p) + return vt.XYZ().Scaled(1 / vt.W) +} + +// TransformDir transforms a direction vector to world space +func (m *T) TransformDir(v vec3.T) vec3.T { + p := vec4.Extend(v, 0) + vt := m.VMul(p) + return vt.XYZ() +} + +// Mul performs a "matrix product" between this matrix and another of the same dimension +func (a *T) Mul(b *T) T { + return T{ + a[0]*b[0] + a[4]*b[1] + a[8]*b[2] + a[12]*b[3], + a[1]*b[0] + a[5]*b[1] + a[9]*b[2] + a[13]*b[3], + a[2]*b[0] + a[6]*b[1] + a[10]*b[2] + a[14]*b[3], + a[3]*b[0] + a[7]*b[1] + a[11]*b[2] + a[15]*b[3], + + a[0]*b[4] + a[4]*b[5] + a[8]*b[6] + a[12]*b[7], + a[1]*b[4] + a[5]*b[5] + a[9]*b[6] + a[13]*b[7], + a[2]*b[4] + a[6]*b[5] + a[10]*b[6] + a[14]*b[7], + a[3]*b[4] + a[7]*b[5] + a[11]*b[6] + a[15]*b[7], + + a[0]*b[8] + a[4]*b[9] + a[8]*b[10] + a[12]*b[11], + a[1]*b[8] + a[5]*b[9] + a[9]*b[10] + a[13]*b[11], + a[2]*b[8] + a[6]*b[9] + a[10]*b[10] + a[14]*b[11], + a[3]*b[8] + a[7]*b[9] + a[11]*b[10] + a[15]*b[11], + + a[0]*b[12] + a[4]*b[13] + a[8]*b[14] + a[12]*b[15], + a[1]*b[12] + a[5]*b[13] + a[9]*b[14] + a[13]*b[15], + a[2]*b[12] + a[6]*b[13] + a[10]*b[14] + a[14]*b[15], + a[3]*b[12] + a[7]*b[13] + a[11]*b[14] + a[15]*b[15], + } +} + +// Transpose produces the transpose of this matrix. For any MxN matrix +// the transpose is an NxM matrix with the rows swapped with the columns. For instance +// the transpose of the Mat3x2 is a Mat2x3 like so: +// +// [[a b]] [[a c e]] +// [[c d]] = [[b d f]] +// [[e f]] +func (m *T) Transpose() T { + return T{ + m[0], m[4], m[8], m[12], + m[1], m[5], m[9], m[13], + m[2], m[6], m[10], m[14], + m[3], m[7], m[11], m[15], + } +} + +// Det returns the determinant of a matrix. It is a measure of a square matrix's +// singularity and invertability, among other things. In this library, the +// determinant is hard coded based on pre-computed cofactor expansion, and uses +// no loops. Of course, the addition and multiplication must still be done. +func (m *T) Det() float32 { + return m[0]*m[5]*m[10]*m[15] - m[0]*m[5]*m[11]*m[14] - m[0]*m[6]*m[9]*m[15] + m[0]*m[6]*m[11]*m[13] + + m[0]*m[7]*m[9]*m[14] - m[0]*m[7]*m[10]*m[13] - m[1]*m[4]*m[10]*m[15] + m[1]*m[4]*m[11]*m[14] + + m[1]*m[6]*m[8]*m[15] - m[1]*m[6]*m[11]*m[12] - m[1]*m[7]*m[8]*m[14] + m[1]*m[7]*m[10]*m[12] + + m[2]*m[4]*m[9]*m[15] - m[2]*m[4]*m[11]*m[13] - m[2]*m[5]*m[8]*m[15] + m[2]*m[5]*m[11]*m[12] + + m[2]*m[7]*m[8]*m[13] - m[2]*m[7]*m[9]*m[12] - m[3]*m[4]*m[9]*m[14] + m[3]*m[4]*m[10]*m[13] + + m[3]*m[5]*m[8]*m[14] - m[3]*m[5]*m[10]*m[12] - m[3]*m[6]*m[8]*m[13] + m[3]*m[6]*m[9]*m[12] +} + +// Invert computes the inverse of a square matrix. An inverse is a square matrix such that when multiplied by the +// original, yields the identity. +// +// M_inv * M = M * M_inv = I +// +// In this library, the math is precomputed, and uses no loops, though the multiplications, additions, determinant calculation, and scaling +// are still done. This can still be (relatively) expensive for a 4x4. +// +// This function checks the determinant to see if the matrix is invertible. +// If the determinant is 0.0, this function returns the zero matrix. However, due to floating point errors, it is +// entirely plausible to get a false positive or negative. +// In the future, an alternate function may be written which takes in a pre-computed determinant. +func (m *T) Invert() T { + det := m.Det() + if math.Equal(det, float32(0.0)) { + return T{} + } + + retMat := T{ + -m[7]*m[10]*m[13] + m[6]*m[11]*m[13] + m[7]*m[9]*m[14] - m[5]*m[11]*m[14] - m[6]*m[9]*m[15] + m[5]*m[10]*m[15], + m[3]*m[10]*m[13] - m[2]*m[11]*m[13] - m[3]*m[9]*m[14] + m[1]*m[11]*m[14] + m[2]*m[9]*m[15] - m[1]*m[10]*m[15], + -m[3]*m[6]*m[13] + m[2]*m[7]*m[13] + m[3]*m[5]*m[14] - m[1]*m[7]*m[14] - m[2]*m[5]*m[15] + m[1]*m[6]*m[15], + m[3]*m[6]*m[9] - m[2]*m[7]*m[9] - m[3]*m[5]*m[10] + m[1]*m[7]*m[10] + m[2]*m[5]*m[11] - m[1]*m[6]*m[11], + m[7]*m[10]*m[12] - m[6]*m[11]*m[12] - m[7]*m[8]*m[14] + m[4]*m[11]*m[14] + m[6]*m[8]*m[15] - m[4]*m[10]*m[15], + -m[3]*m[10]*m[12] + m[2]*m[11]*m[12] + m[3]*m[8]*m[14] - m[0]*m[11]*m[14] - m[2]*m[8]*m[15] + m[0]*m[10]*m[15], + m[3]*m[6]*m[12] - m[2]*m[7]*m[12] - m[3]*m[4]*m[14] + m[0]*m[7]*m[14] + m[2]*m[4]*m[15] - m[0]*m[6]*m[15], + -m[3]*m[6]*m[8] + m[2]*m[7]*m[8] + m[3]*m[4]*m[10] - m[0]*m[7]*m[10] - m[2]*m[4]*m[11] + m[0]*m[6]*m[11], + -m[7]*m[9]*m[12] + m[5]*m[11]*m[12] + m[7]*m[8]*m[13] - m[4]*m[11]*m[13] - m[5]*m[8]*m[15] + m[4]*m[9]*m[15], + m[3]*m[9]*m[12] - m[1]*m[11]*m[12] - m[3]*m[8]*m[13] + m[0]*m[11]*m[13] + m[1]*m[8]*m[15] - m[0]*m[9]*m[15], + -m[3]*m[5]*m[12] + m[1]*m[7]*m[12] + m[3]*m[4]*m[13] - m[0]*m[7]*m[13] - m[1]*m[4]*m[15] + m[0]*m[5]*m[15], + m[3]*m[5]*m[8] - m[1]*m[7]*m[8] - m[3]*m[4]*m[9] + m[0]*m[7]*m[9] + m[1]*m[4]*m[11] - m[0]*m[5]*m[11], + m[6]*m[9]*m[12] - m[5]*m[10]*m[12] - m[6]*m[8]*m[13] + m[4]*m[10]*m[13] + m[5]*m[8]*m[14] - m[4]*m[9]*m[14], + -m[2]*m[9]*m[12] + m[1]*m[10]*m[12] + m[2]*m[8]*m[13] - m[0]*m[10]*m[13] - m[1]*m[8]*m[14] + m[0]*m[9]*m[14], + m[2]*m[5]*m[12] - m[1]*m[6]*m[12] - m[2]*m[4]*m[13] + m[0]*m[6]*m[13] + m[1]*m[4]*m[14] - m[0]*m[5]*m[14], + -m[2]*m[5]*m[8] + m[1]*m[6]*m[8] + m[2]*m[4]*m[9] - m[0]*m[6]*m[9] - m[1]*m[4]*m[10] + m[0]*m[5]*m[10], + } + + return retMat.Scale(1 / det) +} + +// ApproxEqual performs an element-wise approximate equality test between two matrices, +// as if FloatEqual had been used. +func (m *T) ApproxEqual(m2 *T) bool { + for i := range m { + if !math.Equal(m[i], m2[i]) { + return false + } + } + return true +} + +// ApproxEqualThreshold performs an element-wise approximate equality test between two matrices +// with a given epsilon threshold, as if FloatEqualThreshold had been used. +func (m *T) ApproxEqualThreshold(m2 *T, threshold float32) bool { + for i := range m { + if !math.EqualThreshold(m[i], m2[i], threshold) { + return false + } + } + return true +} + +// At returns the matrix element at the given row and column. +// This is equivalent to mat[col * numRow + row] where numRow is constant +// (E.G. for a Mat3x2 it's equal to 3) +// +// This method is garbage-in garbage-out. For instance, on a T asking for +// At(5,0) will work just like At(1,1). Or it may panic if it's out of bounds. +func (m *T) At(row, col int) float32 { + return m[col*4+row] +} + +// Set sets the corresponding matrix element at the given row and column. +func (m *T) Set(row, col int, value float32) { + m[col*4+row] = value +} + +// Index returns the index of the given row and column, to be used with direct +// access. E.G. Index(0,0) = 0. +func (m *T) Index(row, col int) int { + return col*4 + row +} + +// Row returns a vector representing the corresponding row (starting at row 0). +// This package makes no distinction between row and column vectors, so it +// will be a normal VecM for a MxN matrix. +func (m *T) Row(row int) vec4.T { + return vec4.T{ + X: m[row+0], + Y: m[row+4], + Z: m[row+8], + W: m[row+12], + } +} + +// Rows decomposes a matrix into its corresponding row vectors. +// This is equivalent to calling mat.Row for each row. +func (m *T) Rows() (row0, row1, row2, row3 vec4.T) { + return m.Row(0), m.Row(1), m.Row(2), m.Row(3) +} + +// Col returns a vector representing the corresponding column (starting at col 0). +// This package makes no distinction between row and column vectors, so it +// will be a normal VecN for a MxN matrix. +func (m *T) Col(col int) vec4.T { + return vec4.T{ + X: m[col*4+0], + Y: m[col*4+1], + Z: m[col*4+2], + W: m[col*4+3], + } +} + +// Cols decomposes a matrix into its corresponding column vectors. +// This is equivalent to calling mat.Col for each column. +func (m *T) Cols() (col0, col1, col2, col3 vec4.T) { + return m.Col(0), m.Col(1), m.Col(2), m.Col(3) +} + +// Trace is a basic operation on a square matrix that simply +// sums up all elements on the main diagonal (meaning all elements such that row==col). +func (m *T) Trace() float32 { + return m[0] + m[5] + m[10] + m[15] +} + +// Abs returns the element-wise absolute value of this matrix +func (m *T) Abs() T { + return T{ + math.Abs(m[0]), math.Abs(m[1]), math.Abs(m[2]), math.Abs(m[3]), + math.Abs(m[4]), math.Abs(m[5]), math.Abs(m[6]), math.Abs(m[7]), + math.Abs(m[8]), math.Abs(m[9]), math.Abs(m[10]), math.Abs(m[11]), + math.Abs(m[12]), math.Abs(m[13]), math.Abs(m[14]), math.Abs(m[15]), + } +} + +// String pretty prints the matrix +func (m T) String() string { + buf := new(bytes.Buffer) + w := tabwriter.NewWriter(buf, 4, 4, 1, ' ', tabwriter.AlignRight) + for i := 0; i < 4; i++ { + r := m.Row(i) + fmt.Fprintf(w, "%f\t", r.X) + fmt.Fprintf(w, "%f\t", r.Y) + fmt.Fprintf(w, "%f\t", r.Z) + fmt.Fprintf(w, "%f\t", r.W) + } + w.Flush() + return buf.String() +} + +// Right extracts the right vector from a transformation matrix +func (m *T) Right() vec3.T { + return vec3.T{ + X: m[4*0+0], + Y: m[4*1+0], + Z: m[4*2+0], + } +} + +// Up extracts the up vector from a transformation matrix +func (m *T) Up() vec3.T { + return vec3.T{ + X: m[4*0+1], + Y: m[4*1+1], + Z: m[4*2+1], + } +} + +// Forward extracts the forward vector from a transformation matrix +func (m *T) Forward() vec3.T { + return vec3.T{ + X: m[4*0+2], + Y: m[4*1+2], + Z: m[4*2+2], + } +} + +// Origin extracts origin point of the coordinate system represented by the matrix +func (m *T) Origin() vec3.T { + return vec3.T{ + X: m[4*3+0], + Y: m[4*3+1], + Z: m[4*3+2], + } +} diff --git a/plugins/math/mat4/operations.go b/plugins/math/mat4/operations.go new file mode 100644 index 0000000..979ecbb --- /dev/null +++ b/plugins/math/mat4/operations.go @@ -0,0 +1,11 @@ +package mat4 + +// Ident returns a new 4x4 identity matrix +func Ident() T { + return T{ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1, + } +} diff --git a/plugins/math/mat4/project.go b/plugins/math/mat4/project.go new file mode 100644 index 0000000..0f7a33e --- /dev/null +++ b/plugins/math/mat4/project.go @@ -0,0 +1,67 @@ +package mat4 + +import ( + "zworld/plugins/math" + "zworld/plugins/math/vec3" +) + +// Orthographic generates a left-handed orthographic projection matrix. +// Outputs depth values in the range [0, 1] +func Orthographic(left, right, bottom, top, near, far float32) T { + rml, tmb, fmn := (right - left), (top - bottom), (far - near) + return T{ + 2 / rml, 0, 0, 0, + 0, 2 / tmb, 0, 0, + 0, 0, 1 / fmn, 0, + (right + left) / rml, + -(top + bottom) / tmb, + -near / fmn, + 1, + } +} + +// OrthographicRZ generates a left-handed orthographic projection matrix. +// Outputs depth values in the range [1, 0] (reverse Z) +func OrthographicRZ(left, right, bottom, top, near, far float32) T { + rml, tmb, fmn := (right - left), (top - bottom), (near - far) + + return T{ + 2 / rml, 0, 0, 0, + 0, 2 / tmb, 0, 0, + 0, 0, 1 / fmn, 0, + -(right + left) / rml, + -(top + bottom) / tmb, + near / fmn, + 1, + } +} + +// Perspective generates a left-handed perspective projection matrix with reversed depth. +// Outputs depth values in the range [0, 1] +func Perspective(fovy, aspect, near, far float32) T { + fovy = math.DegToRad(fovy) + tanHalfFov := math.Tan(fovy) / 2 + + return T{ + 1 / (aspect * tanHalfFov), 0, 0, 0, + 0, -1 / tanHalfFov, 0, 0, + 0, 0, far / (far - near), 1, + 0, 0, -(far * near) / (far - near), 0, + } +} + +func LookAt(eye, center, up vec3.T) T { + f := center.Sub(eye).Normalized() + r := vec3.Cross(up, f).Normalized() + u := vec3.Cross(f, r) + + M := T{ + r.X, u.X, f.X, 0, + r.Y, u.Y, f.Y, 0, + r.Z, u.Z, f.Z, 0, + 0, 0, 0, 1, + } + + et := Translate(eye.Scaled(-1)) + return M.Mul(&et) +} diff --git a/plugins/math/mat4/project_test.go b/plugins/math/mat4/project_test.go new file mode 100644 index 0000000..c714080 --- /dev/null +++ b/plugins/math/mat4/project_test.go @@ -0,0 +1,56 @@ +package mat4_test + +import ( + "testing" + + . "zworld/plugins/math/mat4" + "zworld/plugins/math/vec3" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMat4(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "math/mat4") +} + +type TransformTest struct { + Input vec3.T + Output vec3.T +} + +func AssertTransforms(t *testing.T, transform T, cases []TransformTest) { + t.Helper() + for _, c := range cases { + point := transform.TransformPoint(c.Input) + if !point.ApproxEqual(c.Output) { + t.Errorf("expected %v was %v", c.Output, point) + } + } +} + +func TestOrthographicRZ(t *testing.T) { + proj := OrthographicRZ(0, 10, 0, 10, -1, 1) + AssertTransforms(t, proj, []TransformTest{ + {vec3.New(5, 5, 0), vec3.New(0, 0, 0.5)}, + {vec3.New(5, 5, 1), vec3.New(0, 0, 0)}, + {vec3.New(5, 5, -1), vec3.New(0, 0, 1)}, + {vec3.New(0, 0, -1), vec3.New(-1, -1, 1)}, + }) +} + +func TestPerspectiveVK(t *testing.T) { + proj := Perspective(45, 1, 1, 100) + AssertTransforms(t, proj, []TransformTest{ + {vec3.New(0, 0, 1), vec3.New(0, 0, 0)}, + {vec3.New(0, 0, 100), vec3.New(0, 0, 1)}, + }) +} + +var _ = Describe("LookAt (LH)", func() { + It("correctly projects", func() { + proj := LookAt(vec3.Zero, vec3.UnitZ, vec3.UnitY) + Expect(proj.Forward().ApproxEqual(vec3.UnitZ)).To(BeTrue()) + }) +}) diff --git a/plugins/math/mat4/translation.go b/plugins/math/mat4/translation.go new file mode 100644 index 0000000..fc0494b --- /dev/null +++ b/plugins/math/mat4/translation.go @@ -0,0 +1,20 @@ +// Based on code from github.com/go-gl/mathgl: +// Copyright 2014 The go-gl Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +package mat4 + +import ( + "zworld/plugins/math/vec3" +) + +// Translate returns a homogeneous (4x4 for 3D-space) Translation matrix that moves a point by Tx units in the x-direction, Ty units in the y-direction, +// and Tz units in the z-direction +func Translate(translation vec3.T) T { + return T{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, translation.X, translation.Y, translation.Z, 1} +} + +// Scale creates a homogeneous 3D scaling matrix +func Scale(scale vec3.T) T { + return T{scale.X, 0, 0, 0, 0, scale.Y, 0, 0, 0, 0, scale.Z, 0, 0, 0, 0, 1} +} diff --git a/plugins/math/math32.go b/plugins/math/math32.go new file mode 100644 index 0000000..e359561 --- /dev/null +++ b/plugins/math/math32.go @@ -0,0 +1,177 @@ +package math + +import ( + "math" + + "golang.org/x/exp/constraints" +) + +// Various useful constants. +var ( + MinNormal = float32(1.1754943508222875e-38) // 1 / 2**(127 - 1) + MinValue = float32(math.SmallestNonzeroFloat32) + MaxValue = float32(math.MaxFloat32) + + InfPos = float32(math.Inf(1)) + InfNeg = float32(math.Inf(-1)) + NaN = float32(math.NaN()) + + E = float32(math.E) + Pi = float32(math.Pi) + PiOver2 = Pi / 2 + PiOver4 = Pi / 4 + Sqrt2 = float32(math.Sqrt2) + + Epsilon = float32(1e-10) +) + +// Abs returns the absolute value of a number +func Abs[T constraints.Float | constraints.Integer](v T) T { + if v < 0 { + return -v + } + return v +} + +// Min returns the smaller of two numbers +func Min[T constraints.Ordered](a, b T) T { + if a < b { + return a + } + return b +} + +// Max returns the greater of two numbers +func Max[T constraints.Ordered](a, b T) T { + if a > b { + return a + } + return b +} + +// Clamp a value between a minimum and a maximum value +func Clamp[T constraints.Ordered](v, min, max T) T { + if v > max { + return max + } + if v < min { + return min + } + return v +} + +// Ceil a number to the closest integer +func Ceil(x float32) float32 { + return float32(math.Ceil(float64(x))) +} + +// Floor a number to the closest integer +func Floor(x float32) float32 { + return float32(math.Floor(float64(x))) +} + +// Mod returns the remainder of a floating point division +func Mod(x, y float32) float32 { + return float32(math.Mod(float64(x), float64(y))) +} + +// Sqrt returns the square root of a number +func Sqrt(x float32) float32 { + return float32(math.Sqrt(float64(x))) +} + +// Sin computes the sine of x +func Sin(x float32) float32 { + return float32(math.Sin(float64(x))) +} + +// Cos computes the cosine of x +func Cos(x float32) float32 { + return float32(math.Cos(float64(x))) +} + +// Tan computes the tangent of x +func Tan(x float32) float32 { + return float32(math.Tan(float64(x))) +} + +func Sincos(x float32) (float32, float32) { + sin, cos := math.Sincos(float64(x)) + return float32(sin), float32(cos) +} + +func Acos(x float32) float32 { + return float32(math.Acos(float64(x))) +} + +func Asin(x float32) float32 { + return float32(math.Asin(float64(x))) +} + +func Atan2(y, x float32) float32 { + return float32(math.Atan2(float64(y), float64(x))) +} + +// Sign returns the sign of x (-1 or 1) +func Sign(x float32) float32 { + if x > 0 { + return 1 + } + return -1 +} + +func Copysign(f, sign float32) float32 { + return float32(math.Copysign(float64(f), float64(sign))) +} + +// DegToRad converts degrees to radians +func DegToRad(deg float32) float32 { + return Pi * deg / 180.0 +} + +// RadToDeg converts radians to degrees +func RadToDeg(rad float32) float32 { + return 180.0 * rad / Pi +} + +// Equal checks two floats for (approximate) equality +func Equal(a, b float32) bool { + return EqualThreshold(a, b, Epsilon) +} + +// EqualThreshold is a utility function to compare floats. +// It's Taken from http://floating-point-gui.de/errors/comparison/ +// +// It is slightly altered to not call Abs when not needed. +// +// This differs from FloatEqual in that it lets you pass in your comparison threshold, so that you can adjust the comparison value to your specific needs +func EqualThreshold(a, b, epsilon float32) bool { + if a == b { // Handles the case of inf or shortcuts the loop when no significant error has accumulated + return true + } + + diff := Abs(a - b) + if a*b == 0 || diff < MinNormal { // If a or b are 0 or both are extremely close to it + return diff < epsilon*epsilon + } + + // Else compare difference + return diff/(Abs(a)+Abs(b)) < epsilon +} + +// Lerp performs linear interpolation between a and b +func Lerp(a, b, f float32) float32 { + return a + f*(b-a) +} + +func Round(f float32) float32 { + return float32(math.Round(float64(f))) +} + +func Snap(f, multiple float32) float32 { + return Ceil(f/multiple) * multiple +} + +func Pow(f, x float32) float32 { + return float32(math.Pow(float64(f), float64(x))) +} diff --git a/plugins/math/noise.go b/plugins/math/noise.go new file mode 100644 index 0000000..361174a --- /dev/null +++ b/plugins/math/noise.go @@ -0,0 +1,28 @@ +package math + +import ( + opensimplex "github.com/ojrac/opensimplex-go" +) + +// Noise utility to sample simplex noise +type Noise struct { + opensimplex.Noise + Seed int + Freq float32 +} + +// NewNoise creates a new noise struct from a seed value and a frequency factor. +func NewNoise(seed int, freq float32) *Noise { + return &Noise{ + Noise: opensimplex.New(int64(seed)), + Seed: seed, + Freq: freq, + } +} + +// Sample the noise at a certain position +func (n *Noise) Sample(x, y, z int) float32 { + // jeez + fx, fy, fz := float64(float32(x)*n.Freq), float64(float32(y)*n.Freq), float64(float32(z)*n.Freq) + return float32(n.Eval3(fx, fy, fz)) +} diff --git a/plugins/math/quat/quat.go b/plugins/math/quat/quat.go new file mode 100644 index 0000000..c132e14 --- /dev/null +++ b/plugins/math/quat/quat.go @@ -0,0 +1,553 @@ +// Based on code from github.com/go-gl/mathgl: +// Copyright 2014 The go-gl Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +package quat + +import ( + "zworld/plugins/math" + "zworld/plugins/math/mat4" + "zworld/plugins/math/vec3" +) + +// RotationOrder is the order in which rotations will be transformed for the +// purposes of AnglesToQuat. +type RotationOrder int + +// The RotationOrder constants represent a series of rotations along the given +// axes for the use of AnglesToQuat. +const ( + XYX RotationOrder = iota + XYZ + XZX + XZY + YXY + YXZ + YZY + YZX + ZYZ + ZYX + ZXZ + ZXY +) + +// T represents a Quaternion, which is an extension of the imaginary numbers; +// there's all sorts of interesting theory behind it. In 3D graphics we mostly +// use it as a cheap way of representing rotation since quaternions are cheaper +// to multiply by, and easier to interpolate than matrices. +// +// A Quaternion has two parts: W, the so-called scalar component, and "V", the +// vector component. The vector component is considered to be the part in 3D +// space, while W (loosely interpreted) is its 4D coordinate. +type T struct { + W float32 + V vec3.T +} + +// Ident returns the quaternion identity: W=1; V=(0,0,0). +// +// As with all identities, multiplying any quaternion by this will yield the same +// quaternion you started with. +func Ident() T { + return T{1., vec3.New(0, 0, 0)} +} + +// Rotate creates an angle from an axis and an angle relative to that axis. +// +// This is cheaper than HomogRotate3D. +func Rotate(angle float32, axis vec3.T) T { + // angle = (float32(math.Pi) * angle) / 180.0 + + c, s := math.Cos(angle/2), math.Sin(angle/2) + + return T{c, axis.Scaled(s)} +} + +// X is a convenient alias for q.V[0] +func (q T) X() float32 { + return q.V.X +} + +// Y is a convenient alias for q.V[1] +func (q T) Y() float32 { + return q.V.Y +} + +// Z is a convenient alias for q.V[2] +func (q T) Z() float32 { + return q.V.X +} + +// Add adds two quaternions. It's no more complicated than +// adding their W and V components. +func (q1 T) Add(q2 T) T { + return T{q1.W + q2.W, q1.V.Add(q2.V)} +} + +// Sub subtracts two quaternions. It's no more complicated than +// subtracting their W and V components. +func (q1 T) Sub(q2 T) T { + return T{q1.W - q2.W, q1.V.Sub(q2.V)} +} + +// Mul multiplies two quaternions. This can be seen as a rotation. Note that +// Multiplication is NOT commutative, meaning q1.Mul(q2) does not necessarily +// equal q2.Mul(q1). +func (q1 T) Mul(q2 T) T { + return T{q1.W*q2.W - vec3.Dot(q1.V, q2.V), vec3.Cross(q1.V, q2.V).Add(q2.V.Scaled(q1.W)).Add(q1.V.Scaled(q2.W))} +} + +// Scale every element of the quaternion by some constant factor. +func (q1 T) Scale(c float32) T { + return T{q1.W * c, vec3.New(q1.V.X*c, q1.V.Y*c, q1.V.Z*c)} +} + +// Conjugate returns the conjugate of a quaternion. Equivalent to +// Quat{q1.W, q1.V.Mul(-1)}. +func (q1 T) Conjugate() T { + return T{q1.W, q1.V.Scaled(-1)} +} + +// Len gives the Length of the quaternion, also known as its Norm. This is the +// same thing as the Len of a Vec4. +func (q1 T) Len() float32 { + return math.Sqrt(q1.W*q1.W + vec3.Dot(q1.V, q1.V)) +} + +// Norm is an alias for Len() since both are very common terms. +func (q1 T) Norm() float32 { + return q1.Len() +} + +// Normalize the quaternion, returning its versor (unit quaternion). +// +// This is the same as normalizing it as a Vec4. +func (q1 T) Normalize() T { + length := q1.Len() + + if math.Equal(1, length) { + return q1 + } + if length == 0 { + return Ident() + } + if length == math.InfPos { + length = math.MaxValue + } + + return T{q1.W * 1 / length, q1.V.Scaled(1 / length)} +} + +// Inverse of a quaternion. The inverse is equivalent +// to the conjugate divided by the square of the length. +// +// This method computes the square norm by directly adding the sum +// of the squares of all terms instead of actually squaring q1.Len(), +// both for performance and precision. +func (q1 T) Inverse() T { + return q1.Conjugate().Scale(1 / q1.Dot(q1)) +} + +// Rotate a vector by the rotation this quaternion represents. +// This will result in a 3D vector. Strictly speaking, this is +// equivalent to q1.v.q* where the "."" is quaternion multiplication and v is interpreted +// as a quaternion with W 0 and V v. In code: +// q1.Mul(Quat{0,v}).Mul(q1.Conjugate()), and +// then retrieving the imaginary (vector) part. +// +// In practice, we hand-compute this in the general case and simplify +// to save a few operations. +func (q1 T) Rotate(v vec3.T) vec3.T { + cross := vec3.Cross(q1.V, v) + // v + 2q_w * (q_v x v) + 2q_v x (q_v x v) + return v.Add(cross.Scaled(2 * q1.W)).Add(vec3.Cross(q1.V.Scaled(2), cross)) +} + +// Mat4 returns the homogeneous 3D rotation matrix corresponding to the +// quaternion. +func (q1 T) Mat4() mat4.T { + w, x, y, z := q1.W, q1.V.X, q1.V.Y, q1.V.Z + return mat4.T{ + 1 - 2*y*y - 2*z*z, 2*x*y + 2*w*z, 2*x*z - 2*w*y, 0, + 2*x*y - 2*w*z, 1 - 2*x*x - 2*z*z, 2*y*z + 2*w*x, 0, + 2*x*z + 2*w*y, 2*y*z - 2*w*x, 1 - 2*x*x - 2*y*y, 0, + 0, 0, 0, 1, + } +} + +// Dot product between two quaternions, equivalent to if this was a Vec4. +func (q1 T) Dot(q2 T) float32 { + return q1.W*q2.W + vec3.Dot(q1.V, q2.V) +} + +// ApproxEqual returns whether the quaternions are approximately equal, as if +// FloatEqual was called on each matching element +func (q1 T) ApproxEqual(q2 T) bool { + return math.Equal(q1.W, q2.W) && q1.V.ApproxEqual(q2.V) +} + +// OrientationEqual returns whether the quaternions represents the same orientation +// +// Different values can represent the same orientation (q == -q) because quaternions avoid singularities +// and discontinuities involved with rotation in 3 dimensions by adding extra dimensions +func (q1 T) OrientationEqual(q2 T) bool { + return q1.OrientationEqualThreshold(q2, math.Epsilon) +} + +// OrientationEqualThreshold returns whether the quaternions represents the same orientation with a given tolerence +func (q1 T) OrientationEqualThreshold(q2 T, epsilon float32) bool { + return math.Abs(q1.Normalize().Dot(q2.Normalize())) > 1-math.Epsilon +} + +// Slerp is *S*pherical *L*inear Int*erp*olation, a method of interpolating +// between two quaternions. This always takes the straightest path on the sphere between +// the two quaternions, and maintains constant velocity. +// +// However, it's expensive and Slerp(q1,q2) is not the same as Slerp(q2,q1) +func Slerp(q1, q2 T, amount float32) T { + q1, q2 = q1.Normalize(), q2.Normalize() + dot := q1.Dot(q2) + + // If the inputs are too close for comfort, linearly interpolate and normalize the result. + if dot > 0.9995 { + return Nlerp(q1, q2, amount) + } + + // This is here for precision errors, I'm perfectly aware that *technically* the dot is bound [-1,1], but since Acos will freak out if it's not (even if it's just a liiiiitle bit over due to normal error) we need to clamp it + dot = math.Clamp(dot, -1, 1) + + theta := math.Acos(dot) * amount + c, s := math.Cos(theta), math.Sin(theta) + rel := q2.Sub(q1.Scale(dot)).Normalize() + + return q1.Scale(c).Add(rel.Scale(s)) +} + +// Lerp is a *L*inear Int*erp*olation between two Quaternions, cheap and simple. +// +// Not excessively useful, but uses can be found. +func Lerp(q1, q2 T, amount float32) T { + return q1.Add(q2.Sub(q1).Scale(amount)) +} + +// Nlerp is a *Normalized* *L*inear Int*erp*olation between two Quaternions. Cheaper than Slerp +// and usually just as good. This is literally Lerp with Normalize() called on it. +// +// Unlike Slerp, constant velocity isn't maintained, but it's much faster and +// Nlerp(q1,q2) and Nlerp(q2,q1) return the same path. You should probably +// use this more often unless you're suffering from choppiness due to the +// non-constant velocity problem. +func Nlerp(q1, q2 T, amount float32) T { + return Lerp(q1, q2, amount).Normalize() +} + +// FromAngles performs a rotation in the specified order. If the order is not +// a valid RotationOrder, this function will panic +// +// The rotation "order" is more of an axis descriptor. For instance XZX would +// tell the function to interpret angle1 as a rotation about the X axis, angle2 about +// the Z axis, and angle3 about the X axis again. +// +// Based off the code for the Matlab function "angle2quat", though this implementation +// only supports 3 single angles as opposed to multiple angles. +func FromAngles(angle1, angle2, angle3 float32, order RotationOrder) T { + var s [3]float32 + var c [3]float32 + + s[0], c[0] = math.Sincos(angle1 / 2) + s[1], c[1] = math.Sincos(angle2 / 2) + s[2], c[2] = math.Sincos(angle3 / 2) + + ret := T{} + switch order { + case ZYX: + ret.W = c[0]*c[1]*c[2] + s[0]*s[1]*s[2] + ret.V = vec3.T{ + X: c[0]*c[1]*s[2] - s[0]*s[1]*c[2], + Y: c[0]*s[1]*c[2] + s[0]*c[1]*s[2], + Z: s[0]*c[1]*c[2] - c[0]*s[1]*s[2], + } + case ZYZ: + ret.W = c[0]*c[1]*c[2] - s[0]*c[1]*s[2] + ret.V = vec3.T{ + X: c[0]*s[1]*s[2] - s[0]*s[1]*c[2], + Y: c[0]*s[1]*c[2] + s[0]*s[1]*s[2], + Z: s[0]*c[1]*c[2] + c[0]*c[1]*s[2], + } + case ZXY: + ret.W = c[0]*c[1]*c[2] - s[0]*s[1]*s[2] + ret.V = vec3.T{ + X: c[0]*s[1]*c[2] - s[0]*c[1]*s[2], + Y: c[0]*c[1]*s[2] + s[0]*s[1]*c[2], + Z: c[0]*s[1]*s[2] + s[0]*c[1]*c[2], + } + case ZXZ: + ret.W = c[0]*c[1]*c[2] - s[0]*c[1]*s[2] + ret.V = vec3.T{ + X: c[0]*s[1]*c[2] + s[0]*s[1]*s[2], + Y: s[0]*s[1]*c[2] - c[0]*s[1]*s[2], + Z: c[0]*c[1]*s[2] + s[0]*c[1]*c[2], + } + case YXZ: + ret.W = c[0]*c[1]*c[2] + s[0]*s[1]*s[2] + ret.V = vec3.T{ + X: c[0]*s[1]*c[2] + s[0]*c[1]*s[2], + Y: s[0]*c[1]*c[2] - c[0]*s[1]*s[2], + Z: c[0]*c[1]*s[2] - s[0]*s[1]*c[2], + } + case YXY: + ret.W = c[0]*c[1]*c[2] - s[0]*c[1]*s[2] + ret.V = vec3.T{ + X: c[0]*s[1]*c[2] + s[0]*s[1]*s[2], + Y: s[0]*c[1]*c[2] + c[0]*c[1]*s[2], + Z: c[0]*s[1]*s[2] - s[0]*s[1]*c[2], + } + case YZX: + ret.W = c[0]*c[1]*c[2] - s[0]*s[1]*s[2] + ret.V = vec3.T{ + X: c[0]*c[1]*s[2] + s[0]*s[1]*c[2], + Y: c[0]*s[1]*s[2] + s[0]*c[1]*c[2], + Z: c[0]*s[1]*c[2] - s[0]*c[1]*s[2], + } + case YZY: + ret.W = c[0]*c[1]*c[2] - s[0]*c[1]*s[2] + ret.V = vec3.T{ + X: s[0]*s[1]*c[2] - c[0]*s[1]*s[2], + Y: c[0]*c[1]*s[2] + s[0]*c[1]*c[2], + Z: c[0]*s[1]*c[2] + s[0]*s[1]*s[2], + } + case XYZ: + ret.W = c[0]*c[1]*c[2] - s[0]*s[1]*s[2] + ret.V = vec3.T{ + X: c[0]*s[1]*s[2] + s[0]*c[1]*c[2], + Y: c[0]*s[1]*c[2] - s[0]*c[1]*s[2], + Z: c[0]*c[1]*s[2] + s[0]*s[1]*c[2], + } + case XYX: + ret.W = c[0]*c[1]*c[2] - s[0]*c[1]*s[2] + ret.V = vec3.T{ + X: c[0]*c[1]*s[2] + s[0]*c[1]*c[2], + Y: c[0]*s[1]*c[2] + s[0]*s[1]*s[2], + Z: s[0]*s[1]*c[2] - c[0]*s[1]*s[2], + } + case XZY: + ret.W = c[0]*c[1]*c[2] + s[0]*s[1]*s[2] + ret.V = vec3.T{ + X: s[0]*c[1]*c[2] - c[0]*s[1]*s[2], + Y: c[0]*c[1]*s[2] - s[0]*s[1]*c[2], + Z: c[0]*s[1]*c[2] + s[0]*c[1]*s[2], + } + case XZX: + ret.W = c[0]*c[1]*c[2] - s[0]*c[1]*s[2] + ret.V = vec3.T{ + X: c[0]*c[1]*s[2] + s[0]*c[1]*c[2], + Y: c[0]*s[1]*s[2] - s[0]*s[1]*c[2], + Z: c[0]*s[1]*c[2] + s[0]*s[1]*s[2], + } + default: + panic("Unsupported rotation order") + } + return ret +} + +// FromMat4 converts a pure rotation matrix into a quaternion +func FromMat4(m mat4.T) T { + // http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm + + if tr := m[0] + m[5] + m[10]; tr > 0 { + s := 0.5 / math.Sqrt(tr+1.0) + return T{ + 0.25 / s, + vec3.T{ + X: (m[6] - m[9]) * s, + Y: (m[8] - m[2]) * s, + Z: (m[1] - m[4]) * s, + }, + } + } + + if (m[0] > m[5]) && (m[0] > m[10]) { + s := 2.0 * math.Sqrt(1.0+m[0]-m[5]-m[10]) + return T{ + (m[6] - m[9]) / s, + vec3.T{ + X: 0.25 * s, + Y: (m[4] + m[1]) / s, + Z: (m[8] + m[2]) / s, + }, + } + } + + if m[5] > m[10] { + s := 2.0 * math.Sqrt(1.0+m[5]-m[0]-m[10]) + return T{ + (m[8] - m[2]) / s, + vec3.T{ + X: (m[4] + m[1]) / s, + Y: 0.25 * s, + Z: (m[9] + m[6]) / s, + }, + } + + } + + s := 2.0 * math.Sqrt(1.0+m[10]-m[0]-m[5]) + return T{ + (m[1] - m[4]) / s, + vec3.T{ + X: (m[8] + m[2]) / s, + Y: (m[9] + m[6]) / s, + Z: 0.25 * s, + }, + } +} + +// LookAtV creates a rotation from an eye vector to a center vector +// +// It assumes the front of the rotated object at Z- and up at Y+ +func LookAtV(eye, center, up vec3.T) T { + // http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-17-quaternions/#I_need_an_equivalent_of_gluLookAt__How_do_I_orient_an_object_towards_a_point__ + // https://bitbucket.org/sinbad/ogre/src/d2ef494c4a2f5d6e2f0f17d3bfb9fd936d5423bb/OgreMain/src/OgreCamera.cpp?at=default#cl-161 + + direction := center.Sub(eye).Normalized() + + // Find the rotation between the front of the object (that we assume towards Z-, + // but this depends on your model) and the desired direction + rotDir := BetweenVectors(vec3.UnitZN, direction) + + // Recompute up so that it's perpendicular to the direction + // You can skip that part if you really want to force up + //right := direction.Cross(up) + //up = right.Cross(direction) + + // Because of the 1rst rotation, the up is probably completely screwed up. + // Find the rotation between the "up" of the rotated object, and the desired up + upCur := rotDir.Rotate(vec3.Zero) + rotUp := BetweenVectors(upCur, up) + + rotTarget := rotUp.Mul(rotDir) // remember, in reverse order. + return rotTarget.Inverse() // camera rotation should be inversed! +} + +// BetweenVectors calculates the rotation between two vectors +func BetweenVectors(start, dest vec3.T) T { + // http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-17-quaternions/#I_need_an_equivalent_of_gluLookAt__How_do_I_orient_an_object_towards_a_point__ + // https://github.com/g-truc/glm/blob/0.9.5/glm/gtx/quaternion.inl#L225 + // https://bitbucket.org/sinbad/ogre/src/d2ef494c4a2f5d6e2f0f17d3bfb9fd936d5423bb/OgreMain/include/OgreVector3.h?at=default#cl-654 + + start = start.Normalized() + dest = dest.Normalized() + epsilon := float32(0.001) + + cosTheta := vec3.Dot(start, dest) + if cosTheta < -1.0+epsilon { + // special case when vectors in opposite directions: + // there is no "ideal" rotation axis + // So guess one; any will do as long as it's perpendicular to start + axis := vec3.Cross(vec3.UnitX, start) + if vec3.Dot(axis, axis) < epsilon { + // bad luck, they were parallel, try again! + axis = vec3.Cross(vec3.UnitY, start) + } + + return Rotate(math.Pi, axis.Normalized()) + } + + axis := vec3.Cross(start, dest) + s := float32(math.Sqrt(float32(1.0+cosTheta) * 2.0)) + + return T{ + s * 0.5, + axis.Scaled(1.0 / s), + } +} + +func (q T) ToAngles(order RotationOrder) vec3.T { + // this function was adapted from a Go port of Three.js math, github.com/tengge1/go-three-math + // Copyright 2017-2020 The ShadowEditor Authors. All rights reserved. + // Use of e source code is governed by a MIT-style + // license that can be found in the LICENSE file. + + // assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled) + te := q.Mat4() + m11, m12, m13 := te[0], te[4], te[8] + m21, m22, m23 := te[1], te[5], te[9] + m31, m32, m33 := te[2], te[6], te[10] + + e := vec3.Zero + switch order { + default: + panic("unsupported rotation order") + case XYZ: + e.Y = math.Asin(math.Clamp(m13, -1, 1)) + + if math.Abs(m13) < 0.9999999 { + e.X = math.Atan2(-m23, m33) + e.Z = math.Atan2(-m12, m11) + } else { + e.X = math.Atan2(m32, m22) + e.Z = 0 + } + case YXZ: + e.X = math.Asin(-math.Clamp(m23, -1, 1)) + + if math.Abs(m23) < 0.9999999 { + e.Y = math.Atan2(m13, m33) + e.Z = math.Atan2(m21, m22) + } else { + e.Y = math.Atan2(-m31, m11) + e.Z = 0 + } + case ZXY: + e.X = math.Asin(math.Clamp(m32, -1, 1)) + + if math.Abs(m32) < 0.9999999 { + e.Y = math.Atan2(-m31, m33) + e.Z = math.Atan2(-m12, m22) + } else { + e.Y = 0 + e.Z = math.Atan2(m21, m11) + } + case ZYX: + e.Y = math.Asin(-math.Clamp(m31, -1, 1)) + + if math.Abs(m31) < 0.9999999 { + e.X = math.Atan2(m32, m33) + e.Z = math.Atan2(m21, m11) + } else { + e.X = 0 + e.Z = math.Atan2(-m12, m22) + } + case YZX: + e.Z = math.Asin(math.Clamp(m21, -1, 1)) + + if math.Abs(m21) < 0.9999999 { + e.X = math.Atan2(-m23, m22) + e.Y = math.Atan2(-m31, m11) + } else { + e.X = 0 + e.Y = math.Atan2(m13, m33) + } + case XZY: + e.Z = math.Asin(-math.Clamp(m12, -1, 1)) + + if math.Abs(m12) < 0.9999999 { + e.X = math.Atan2(m32, m22) + e.Y = math.Atan2(m13, m11) + } else { + e.X = math.Atan2(-m23, m33) + e.Y = 0 + } + } + + return e +} + +func (q T) Euler() vec3.T { + // convert radians to degrees + return q.ToAngles(YXZ).Scaled(180.0 / math.Pi) +} + +func Euler(x, y, z float32) T { + return FromAngles(math.DegToRad(y), math.DegToRad(x), math.DegToRad(z), YXZ) +} diff --git a/plugins/math/quat/quat_suite_test.go b/plugins/math/quat/quat_suite_test.go new file mode 100644 index 0000000..0410d31 --- /dev/null +++ b/plugins/math/quat/quat_suite_test.go @@ -0,0 +1,28 @@ +package quat_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "testing" + + "zworld/plugins/math/quat" + "zworld/plugins/math/vec3" +) + +func TestQuat(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "math/quat") +} + +var _ = Describe("quaternion", func() { + Context("euler angles", func() { + It("converts back and forth", func() { + x, y, z := float32(10), float32(20), float32(30) + q := quat.Euler(x, y, z) + r := q.Euler() + GinkgoWriter.Println(x, y, z, r) + Expect(r).To(ApproxVec3(vec3.New(x, y, z)), "wrong rotation") + }) + }) +}) diff --git a/plugins/math/random/random.go b/plugins/math/random/random.go new file mode 100644 index 0000000..b909b53 --- /dev/null +++ b/plugins/math/random/random.go @@ -0,0 +1,28 @@ +package random + +import ( + "math/rand" + "time" +) + +func init() { + seed := time.Now().Nanosecond() + Seed(seed) +} + +func Seed(seed int) { + rand.Seed(int64(seed)) +} + +func Range(min, max float32) float32 { + return min + rand.Float32()*(max-min) +} + +func Chance(chance float32) bool { + return Range(0, 1) <= chance +} + +func Choice[T any](slice []T) T { + idx := rand.Intn(len(slice)) + return slice[idx] +} diff --git a/plugins/math/shape/frustum.go b/plugins/math/shape/frustum.go new file mode 100644 index 0000000..bd10ea8 --- /dev/null +++ b/plugins/math/shape/frustum.go @@ -0,0 +1,107 @@ +package shape + +import ( + "zworld/plugins/math/mat4" + "zworld/plugins/math/vec3" +) + +type Plane struct { + Normal vec3.T + Distance float32 +} + +func (p *Plane) normalize() { + length := p.Normal.LengthSqr() + p.Distance*p.Distance + p.Normal = p.Normal.Scaled(1 / length) + p.Distance /= length +} + +func (p *Plane) DistanceToPoint(point vec3.T) float32 { + return vec3.Dot(p.Normal, point) + p.Distance +} + +type Frustum struct { + Front, Back, Left, Right, Top, Bottom Plane +} + +func (f *Frustum) IntersectsSphere(s *Sphere) bool { + if f.Left.DistanceToPoint(s.Center) <= -s.Radius { + return false + } + if f.Right.DistanceToPoint(s.Center) <= -s.Radius { + return false + } + if f.Top.DistanceToPoint(s.Center) <= -s.Radius { + return false + } + if f.Bottom.DistanceToPoint(s.Center) <= -s.Radius { + return false + } + if f.Front.DistanceToPoint(s.Center) <= -s.Radius { + return false + } + if f.Back.DistanceToPoint(s.Center) <= -s.Radius { + return false + } + return true +} + +func FrustumFromMatrix(vp mat4.T) Frustum { + f := Frustum{ + Left: Plane{ + Normal: vec3.T{ + X: vp[0+3] + vp[0+0], + Y: vp[4+3] + vp[4+0], + Z: vp[8+3] + vp[8+0], + }, + Distance: vp[12+3] + vp[12+0], + }, + Right: Plane{ + Normal: vec3.T{ + X: vp[0+3] - vp[0+0], + Y: vp[4+3] - vp[4+0], + Z: vp[8+3] - vp[8+0], + }, + Distance: vp[12+3] - vp[12+0], + }, + Top: Plane{ + Normal: vec3.T{ + X: vp[0+3] - vp[0+1], + Y: vp[4+3] - vp[4+1], + Z: vp[8+3] - vp[8+1], + }, + Distance: vp[12+3] - vp[12+1], + }, + Bottom: Plane{ + Normal: vec3.T{ + X: vp[0+3] + vp[0+1], + Y: vp[4+3] + vp[4+1], + Z: vp[8+3] + vp[8+1], + }, + Distance: vp[12+3] + vp[12+1], + }, + Back: Plane{ + Normal: vec3.T{ + X: vp[0+3] + vp[0+2], + Y: vp[4+3] + vp[4+2], + Z: vp[8+3] + vp[8+2], + }, + Distance: vp[12+3] + vp[12+2], + }, + Front: Plane{ + Normal: vec3.T{ + X: vp[0+3] - vp[0+2], + Y: vp[4+3] - vp[4+2], + Z: vp[8+3] - vp[8+2], + }, + Distance: vp[12+3] - vp[12+2], + }, + } + f.Front.normalize() + f.Back.normalize() + f.Top.normalize() + f.Bottom.normalize() + f.Left.normalize() + f.Right.normalize() + return f +} diff --git a/plugins/math/shape/sphere.go b/plugins/math/shape/sphere.go new file mode 100644 index 0000000..444ccbc --- /dev/null +++ b/plugins/math/shape/sphere.go @@ -0,0 +1,15 @@ +package shape + +import "zworld/plugins/math/vec3" + +type Sphere struct { + Center vec3.T + Radius float32 +} + +func (s *Sphere) IntersectsSphere(other *Sphere) bool { + sepAxis := s.Center.Sub(other.Center) + radiiSum := s.Radius + other.Radius + intersects := sepAxis.LengthSqr() < (radiiSum * radiiSum) + return intersects +} diff --git a/plugins/math/transform/transform.go b/plugins/math/transform/transform.go new file mode 100644 index 0000000..c59ec47 --- /dev/null +++ b/plugins/math/transform/transform.go @@ -0,0 +1,273 @@ +package transform + +import ( + "zworld/plugins/math/mat4" + "zworld/plugins/math/quat" + "zworld/plugins/math/vec3" + "zworld/plugins/system/events" +) + +type T interface { + Forward() vec3.T + Right() vec3.T + Up() vec3.T + + Position() vec3.T + SetPosition(vec3.T) + + Rotation() quat.T + SetRotation(quat.T) + + Scale() vec3.T + SetScale(vec3.T) + + Matrix() mat4.T + + ProjectDir(dir vec3.T) vec3.T + + // Project local coordinates to world coordinates + Project(point vec3.T) vec3.T + + // Unproject world coordinates to local coordinates + Unproject(point vec3.T) vec3.T + + UnprojectDir(dir vec3.T) vec3.T + + WorldPosition() vec3.T + SetWorldPosition(vec3.T) + + WorldScale() vec3.T + SetWorldScale(vec3.T) + + WorldRotation() quat.T + SetWorldRotation(quat.T) + + Parent() T + SetParent(T) + OnChange() *events.Event[T] +} + +// Transform represents a 3D transformation +type transform struct { + position vec3.T + scale vec3.T + rotation quat.T + + wposition vec3.T + wscale vec3.T + wrotation quat.T + + matrix mat4.T + right vec3.T + up vec3.T + forward vec3.T + + inv *mat4.T + dirty bool + parent T + changed events.Event[T] + unsub func() +} + +// NewTransform creates a new 3D transform +func New(position vec3.T, rotation quat.T, scale vec3.T) T { + t := &transform{ + matrix: mat4.Ident(), + position: position, + rotation: rotation, + scale: scale, + dirty: true, + } + t.refresh() + return t +} + +// Identity returns a new transform that does nothing. +func Identity() T { + return New(vec3.Zero, quat.Ident(), vec3.One) +} + +func (t *transform) Parent() T { return t.parent } +func (t *transform) SetParent(parent T) { + // check for cycles + ancestor := parent + for ancestor != nil { + if ancestor.(T) == t { + panic("cyclical transform hierarchies are not allowed") + } + ancestor = ancestor.Parent() + } + + // todo: we might want to maintain world transform when attaching/detaching + + // detach from previous parent (if any) + if t.unsub != nil { + // unsub + t.unsub() + t.unsub = nil + } + + t.parent = parent + + // attach to new parent (if any) + if t.parent != nil { + t.unsub = t.parent.OnChange().Subscribe(func(parent T) { + // mark as dirty on parent change + t.invalidate() + }) + } + + t.invalidate() +} + +func (t *transform) OnChange() *events.Event[T] { + return &t.changed +} + +func (t *transform) invalidate() { + t.dirty = true + t.changed.Emit(t) +} + +// Update transform matrix and its right/up/forward vectors +func (t *transform) refresh() { + if !t.dirty { + return + } + + position := t.position + rotation := t.rotation + scale := t.scale + + if t.parent != nil { + scale = scale.Mul(t.parent.WorldScale()) + + rotation = rotation.Mul(t.parent.WorldRotation()) + + position = t.parent.WorldRotation().Rotate(t.parent.WorldScale().Mul(position)) + position = t.parent.WorldPosition().Add(position) + } + + // calculate basis vectors + t.right = rotation.Rotate(vec3.Right) + t.up = rotation.Rotate(vec3.Up) + t.forward = rotation.Rotate(vec3.Forward) + + // apply scaling + x := t.right.Scaled(scale.X) + y := t.up.Scaled(scale.Y) + z := t.forward.Scaled(scale.Z) + + // create transformation matrix + p := position + t.matrix = mat4.T{ + x.X, x.Y, x.Z, 0, + y.X, y.Y, y.Z, 0, + z.X, z.Y, z.Z, 0, + p.X, p.Y, p.Z, 1, + } + + // save world transforms + t.wposition = position + t.wscale = scale + t.wrotation = rotation + + // mark as clean + t.dirty = false + + // clear inversion cache + t.inv = nil +} + +func (t *transform) inverse() *mat4.T { + t.refresh() + if t.inv == nil { + inv := t.matrix.Invert() + t.inv = &inv + } + return t.inv +} + +func (t *transform) Project(point vec3.T) vec3.T { + t.refresh() + return t.matrix.TransformPoint(point) +} + +func (t *transform) Unproject(point vec3.T) vec3.T { + return t.inverse().TransformPoint(point) +} + +func (t *transform) ProjectDir(dir vec3.T) vec3.T { + t.refresh() + return t.matrix.TransformDir(dir) +} + +func (t *transform) UnprojectDir(dir vec3.T) vec3.T { + return t.inverse().TransformDir(dir) +} + +func (t *transform) WorldPosition() vec3.T { t.refresh(); return t.wposition } +func (t *transform) WorldScale() vec3.T { t.refresh(); return t.wscale } +func (t *transform) WorldRotation() quat.T { t.refresh(); return t.wrotation } + +func (t *transform) SetWorldPosition(wp vec3.T) { + t.refresh() + if t.parent != nil { + t.position = t.parent.Unproject(wp) + } else { + t.position = wp + } + t.invalidate() +} + +func (t *transform) SetWorldScale(wscale vec3.T) { + t.refresh() + if t.parent != nil { + // world_scale = parent_scale * local_scale + // local_scale = world_scale / parent_scale + t.scale = wscale.Div(t.parent.WorldScale()) + } else { + t.scale = wscale + } + t.invalidate() +} + +func (t *transform) SetWorldRotation(rot quat.T) { + t.refresh() + if t.parent != nil { + t.rotation = rot.Mul(t.parent.WorldRotation().Inverse()) + } else { + t.rotation = rot + } + t.invalidate() +} + +func (t *transform) Matrix() mat4.T { t.refresh(); return t.matrix } +func (t *transform) Right() vec3.T { t.refresh(); return t.right } +func (t *transform) Up() vec3.T { t.refresh(); return t.up } +func (t *transform) Forward() vec3.T { t.refresh(); return t.forward } + +func (t *transform) Position() vec3.T { t.refresh(); return t.position } +func (t *transform) Rotation() quat.T { t.refresh(); return t.rotation } +func (t *transform) Scale() vec3.T { t.refresh(); return t.scale } +func (t *transform) SetPosition(p vec3.T) { t.position = p; t.invalidate() } +func (t *transform) SetRotation(r quat.T) { t.rotation = r; t.invalidate() } +func (t *transform) SetScale(s vec3.T) { t.scale = s; t.invalidate() } + +func Matrix(position vec3.T, rotation quat.T, scale vec3.T) mat4.T { + x := rotation.Rotate(vec3.Right) + y := rotation.Rotate(vec3.Up) + z := rotation.Rotate(vec3.Forward) + + x.Scale(scale.X) + y.Scale(scale.Y) + z.Scale(scale.Z) + + p := position + return mat4.T{ + x.X, x.Y, x.Z, 0, + y.X, y.Y, y.Z, 0, + z.X, z.Y, z.Z, 0, + p.X, p.Y, p.Z, 1, + } +} diff --git a/plugins/math/vec2/array.go b/plugins/math/vec2/array.go new file mode 100644 index 0000000..40d84c4 --- /dev/null +++ b/plugins/math/vec2/array.go @@ -0,0 +1,21 @@ +package vec2 + +import "unsafe" + +// Array holds an array of 2-component vectors +type Array []T + +// Elements returns the number of elements in the array +func (a Array) Elements() int { + return len(a) +} + +// Size return the byte size of an element +func (a Array) Size() int { + return 8 +} + +// Pointer returns an unsafe pointer to the first element in the array +func (a Array) Pointer() unsafe.Pointer { + return unsafe.Pointer(&a[0]) +} diff --git a/plugins/math/vec2/operations.go b/plugins/math/vec2/operations.go new file mode 100644 index 0000000..8f2f6c5 --- /dev/null +++ b/plugins/math/vec2/operations.go @@ -0,0 +1,37 @@ +package vec2 + +import "zworld/plugins/math" + +// New returns a vec2 from its components +func New(x, y float32) T { + return T{X: x, Y: y} +} + +// NewI returns a vec2 from integer components +func NewI(x, y int) T { + return T{X: float32(x), Y: float32(y)} +} + +// Dot returns the dot product of two vectors. +func Dot(a, b T) float32 { + return a.X*b.X + a.Y*b.Y +} + +// Distance returns the euclidian distance between two points. +func Distance(a, b T) float32 { + return a.Sub(b).Length() +} + +func Min(a, b T) T { + return T{ + X: math.Min(a.X, b.X), + Y: math.Min(a.Y, b.Y), + } +} + +func Max(a, b T) T { + return T{ + X: math.Max(a.X, b.X), + Y: math.Max(a.Y, b.Y), + } +} diff --git a/plugins/math/vec2/vec2.go b/plugins/math/vec2/vec2.go new file mode 100644 index 0000000..cbbbba5 --- /dev/null +++ b/plugins/math/vec2/vec2.go @@ -0,0 +1,134 @@ +package vec2 + +import ( + "fmt" + + "zworld/plugins/math" +) + +var ( + // Zero is the zero vector + Zero = T{0, 0} + + // One is the one vector + One = T{1, 1} + + // UnitX is the unit vector in the X direction + UnitX = T{1, 0} + + // UnitY is the unit vector in the Y direction + UnitY = T{0, 1} + + InfPos = T{math.InfPos, math.InfPos} + InfNeg = T{math.InfNeg, math.InfNeg} +) + +// T holds a 2-component vector of 32-bit floats +type T struct { + X, Y float32 +} + +// Slice converts the vector into a 2-element slice of float32 +func (v T) Slice() [2]float32 { + return [2]float32{v.X, v.Y} +} + +// Length returns the length of the vector. +// See also LengthSqr and Normalize. +func (v T) Length() float32 { + return math.Sqrt(v.LengthSqr()) +} + +// LengthSqr returns the squared length of the vector. +// See also Length and Normalize. +func (v T) LengthSqr() float32 { + return v.X*v.X + v.Y*v.Y +} + +// Abs sets every component of the vector to its absolute value. +func (v T) Abs() T { + return T{math.Abs(v.X), math.Abs(v.Y)} +} + +// Normalize normalizes the vector to unit length. +func (v *T) Normalize() { + sl := v.LengthSqr() + if sl == 0 || sl == 1 { + return + } + s := 1 / math.Sqrt(sl) + v.X *= s + v.Y *= s +} + +// Normalized returns a unit length normalized copy of the vector. +func (v T) Normalized() T { + v.Normalize() + return v +} + +// Scaled returns a scaled copy of the vector. +func (v T) Scaled(f float32) T { + return T{v.X * f, v.Y * f} +} + +// Scale the vector by a constant (in-place) +func (v *T) Scale(f float32) { + v.X *= f + v.Y *= f +} + +// Swap returns a new vector with components swapped. +func (v T) Swap() T { + return T{v.Y, v.X} +} + +// Invert components in place +func (v *T) Invert() { + v.X = -v.X + v.Y = -v.Y +} + +// Inverted returns a new vector with inverted components +func (v T) Inverted() T { + return T{-v.X, -v.Y} +} + +// Add each element of the vector with the corresponding element of another vector +func (v T) Add(v2 T) T { + return T{v.X + v2.X, v.Y + v2.Y} +} + +// Sub subtracts each element of the vector with the corresponding element of another vector +func (v T) Sub(v2 T) T { + return T{v.X - v2.X, v.Y - v2.Y} +} + +// Mul multiplies each element of the vector with the corresponding element of another vector +func (v T) Mul(v2 T) T { + return T{v.X * v2.X, v.Y * v2.Y} +} + +// Div divides each element of the vector with the corresponding element of another vector +func (v T) Div(v2 T) T { + return T{v.X / v2.X, v.Y / v2.Y} +} + +func (v T) ApproxEqual(v2 T) bool { + epsilon := float32(0.0001) + return Distance(v, v2) < epsilon +} + +func (v T) String() string { + return fmt.Sprintf("%.3f,%.3f", v.X, v.Y) +} + +// Floor each components of the vector +func (v T) Floor() T { + return T{math.Floor(v.X), math.Floor(v.Y)} +} + +// Ceil each component of the vector +func (v T) Ceil() T { + return T{math.Ceil(v.X), math.Ceil(v.Y)} +} diff --git a/plugins/math/vec3/array.go b/plugins/math/vec3/array.go new file mode 100644 index 0000000..c54c3d3 --- /dev/null +++ b/plugins/math/vec3/array.go @@ -0,0 +1,21 @@ +package vec3 + +import "unsafe" + +// Array holds an array of 3-component vectors +type Array []T + +// Elements returns the number of elements in the array +func (a Array) Elements() int { + return len(a) +} + +// Size return the byte size of an element +func (a Array) Size() int { + return 12 +} + +// Pointer returns an unsafe pointer to the first element in the array +func (a Array) Pointer() unsafe.Pointer { + return unsafe.Pointer(&a[0]) +} diff --git a/plugins/math/vec3/operations.go b/plugins/math/vec3/operations.go new file mode 100644 index 0000000..4a3535c --- /dev/null +++ b/plugins/math/vec3/operations.go @@ -0,0 +1,85 @@ +package vec3 + +import ( + "zworld/plugins/math" + "zworld/plugins/math/random" + "zworld/plugins/math/vec2" +) + +// New returns a Vec3 from its components +func New(x, y, z float32) T { + return T{x, y, z} +} + +func New1(v float32) T { + return T{v, v, v} +} + +// NewI returns a Vec3 from integer components +func NewI(x, y, z int) T { + return T{float32(x), float32(y), float32(z)} +} + +func NewI1(v int) T { + return T{float32(v), float32(v), float32(v)} +} + +func FromSlice(v []float32) T { + if len(v) < 3 { + panic("slice must have at least 3 components") + } + return T{v[0], v[1], v[2]} +} + +// Extend a vec2 to a vec3 by adding a Z component +func Extend(v vec2.T, z float32) T { + return T{v.X, v.Y, z} +} + +// Dot returns the dot product of two vectors. +func Dot(a, b T) float32 { + return a.X*b.X + a.Y*b.Y + a.Z*b.Z +} + +// Cross returns the cross product of two vectors. +func Cross(a, b T) T { + return T{ + a.Y*b.Z - a.Z*b.Y, + a.Z*b.X - a.X*b.Z, + a.X*b.Y - a.Y*b.X, + } +} + +// Distance returns the euclidian distance between two points. +func Distance(a, b T) float32 { + return a.Sub(b).Length() +} + +func Mid(a, b T) T { + return a.Add(b).Scaled(0.5) +} + +// Random vector, not normalized. +func Random(min, max T) T { + return T{ + random.Range(min.X, max.X), + random.Range(min.Y, max.Y), + random.Range(min.Z, max.Z), + } +} + +func Min(a, b T) T { + return T{ + X: math.Min(a.X, b.X), + Y: math.Min(a.Y, b.Y), + Z: math.Min(a.Z, b.Z), + } +} + +func Max(a, b T) T { + return T{ + X: math.Max(a.X, b.X), + Y: math.Max(a.Y, b.Y), + Z: math.Max(a.Z, b.Z), + } +} diff --git a/plugins/math/vec3/vec3.go b/plugins/math/vec3/vec3.go new file mode 100644 index 0000000..8781291 --- /dev/null +++ b/plugins/math/vec3/vec3.go @@ -0,0 +1,205 @@ +package vec3 + +import ( + "fmt" + "zworld/plugins/math" + + "zworld/plugins/math/vec2" +) + +var ( + // Zero is the zero vector + Zero = T{0, 0, 0} + + // One is the unit vector + One = T{1, 1, 1} + + // UnitX is the unit vector in the X direction (right) + UnitX = T{1, 0, 0} + Right = T{1, 0, 0} + + // UnitXN is the unit vector in the negative X direction (left) + UnitXN = T{-1, 0, 0} + Left = T{-1, 0, 0} + + // UnitY is the unit vector in the Y direction (up) + UnitY = T{0, 1, 0} + Up = T{0, 1, 0} + + // UnitYN is the unit vector in the negative Y direction (down) + UnitYN = T{0, -1, 0} + Down = T{0, -1, 0} + + // UnitZ is the unit vector in the Z direction (forward) + UnitZ = T{0, 0, 1} + Forward = T{0, 0, 1} + + // UnitZN is the unit vector in the negative Z direction (backward) + UnitZN = T{0, 0, -1} + Backward = T{0, 0, 1} + + InfPos = T{math.InfPos, math.InfPos, math.InfPos} + InfNeg = T{math.InfNeg, math.InfNeg, math.InfNeg} +) + +// T holds a 3-component vector of 32-bit floats +type T struct { + X, Y, Z float32 +} + +// Slice converts the vector into a 3-element slice of float32 +func (v T) Slice() [3]float32 { + return [3]float32{v.X, v.Y, v.Z} +} + +// Length returns the length of the vector. +// See also LengthSqr and Normalize. +func (v T) Length() float32 { + return math.Sqrt(v.LengthSqr()) +} + +// LengthSqr returns the squared length of the vector. +// See also Length and Normalize. +func (v T) LengthSqr() float32 { + return v.X*v.X + v.Y*v.Y + v.Z*v.Z +} + +// Abs returns a copy containing the absolute values of the vector components. +func (v T) Abs() T { + return T{math.Abs(v.X), math.Abs(v.Y), math.Abs(v.Z)} +} + +// Normalize normalizes the vector to unit length. +func (v *T) Normalize() { + sl := v.LengthSqr() + if sl == 0 || sl == 1 { + return + } + s := 1 / math.Sqrt(sl) + v.X *= s + v.Y *= s + v.Z *= s +} + +// Normalized returns a unit length normalized copy of the vector. +func (v T) Normalized() T { + v.Normalize() + return v +} + +// Scale the vector by a constant (in-place) +func (v *T) Scale(f float32) { + v.X *= f + v.Y *= f + v.Z *= f +} + +// Scaled returns a scaled vector +func (v T) Scaled(f float32) T { + return T{v.X * f, v.Y * f, v.Z * f} +} + +// ScaleI returns a vector scaled by an integer factor +func (v T) ScaleI(i int) T { + return v.Scaled(float32(i)) +} + +// Invert the vector components +func (v *T) Invert() { + v.X = -v.X + v.Y = -v.Y + v.Z = -v.Z +} + +// Inverted returns an inverted vector +func (v *T) Inverted() T { + i := *v + i.Invert() + return i +} + +// Floor each components of the vector +func (v T) Floor() T { + return T{math.Floor(v.X), math.Floor(v.Y), math.Floor(v.Z)} +} + +// Ceil each component of the vector +func (v T) Ceil() T { + return T{math.Ceil(v.X), math.Ceil(v.Y), math.Ceil(v.Z)} +} + +// Round each component of the vector +func (v T) Round() T { + return T{math.Round(v.X), math.Round(v.Y), math.Round(v.Z)} +} + +// Add each element of the vector with the corresponding element of another vector +func (v T) Add(v2 T) T { + return T{ + v.X + v2.X, + v.Y + v2.Y, + v.Z + v2.Z, + } +} + +// Sub subtracts each element of the vector with the corresponding element of another vector +func (v T) Sub(v2 T) T { + return T{ + v.X - v2.X, + v.Y - v2.Y, + v.Z - v2.Z, + } +} + +// Mul multiplies each element of the vector with the corresponding element of another vector +func (v T) Mul(v2 T) T { + return T{ + v.X * v2.X, + v.Y * v2.Y, + v.Z * v2.Z, + } +} + +// XY returns a 2-component vector with the X, Y components of this vector +func (v T) XY() vec2.T { + return vec2.T{X: v.X, Y: v.Y} +} + +// XZ returns a 2-component vector with the X, Z components of this vector +func (v T) XZ() vec2.T { + return vec2.T{X: v.X, Y: v.Z} +} + +// YZ returns a 2-component vector with the Y, Z components of this vector +func (v T) YZ() vec2.T { + return vec2.T{X: v.Y, Y: v.Z} +} + +// Div divides each element of the vector with the corresponding element of another vector +func (v T) Div(v2 T) T { + return T{v.X / v2.X, v.Y / v2.Y, v.Z / v2.Z} +} + +// WithX returns a new vector with the X component set to a given value +func (v T) WithX(x float32) T { + return T{x, v.Y, v.Z} +} + +// WithY returns a new vector with the Y component set to a given value +func (v T) WithY(y float32) T { + return T{v.X, y, v.Z} +} + +// WithZ returns a new vector with the Z component set to a given value +func (v T) WithZ(z float32) T { + return T{v.X, v.Y, z} +} + +func (v T) ApproxEqual(v2 T) bool { + epsilon := float32(0.0001) + return Distance(v, v2) < epsilon +} + +func (v T) String() string { + return fmt.Sprintf("%.3f,%.3f,%.3f", v.X, v.Y, v.Z) +} diff --git a/plugins/math/vec4/array.go b/plugins/math/vec4/array.go new file mode 100644 index 0000000..57989cb --- /dev/null +++ b/plugins/math/vec4/array.go @@ -0,0 +1,21 @@ +package vec4 + +import "unsafe" + +// Array holds an array of 4-component vectors +type Array []T + +// Elements returns the number of elements in the array +func (a Array) Elements() int { + return len(a) +} + +// Size return the byte size of an element +func (a Array) Size() int { + return 16 +} + +// Pointer returns an unsafe pointer to the first element in the array +func (a Array) Pointer() unsafe.Pointer { + return unsafe.Pointer(&a[0]) +} diff --git a/plugins/math/vec4/operations.go b/plugins/math/vec4/operations.go new file mode 100644 index 0000000..d668999 --- /dev/null +++ b/plugins/math/vec4/operations.go @@ -0,0 +1,56 @@ +package vec4 + +import ( + "zworld/plugins/math" + "zworld/plugins/math/random" + "zworld/plugins/math/vec2" + "zworld/plugins/math/vec3" +) + +// New returns a new vec4 from its components +func New(x, y, z, w float32) T { + return T{x, y, z, w} +} + +// Extend a vec3 to a vec4 by adding a W component +func Extend(v vec3.T, w float32) T { + return T{v.X, v.Y, v.Z, w} +} + +// Extend2 a vec2 to a vec4 by adding the Z and W components +func Extend2(v vec2.T, z, w float32) T { + return T{v.X, v.Y, z, w} +} + +// Dot returns the dot product of two vectors. +func Dot(a, b T) float32 { + return a.X*b.X + a.Y*b.Y + a.Z*b.Z + a.W*b.W +} + +// Random vector, not normalized. +func Random(min, max T) T { + return T{ + random.Range(min.X, max.X), + random.Range(min.Y, max.Y), + random.Range(min.Z, max.Z), + random.Range(min.W, max.W), + } +} + +func Min(a, b T) T { + return T{ + X: math.Min(a.X, b.X), + Y: math.Min(a.Y, b.Y), + Z: math.Min(a.Z, b.Z), + W: math.Min(a.W, b.W), + } +} + +func Max(a, b T) T { + return T{ + X: math.Max(a.X, b.X), + Y: math.Max(a.Y, b.Y), + Z: math.Max(a.Z, b.Z), + W: math.Max(a.W, b.W), + } +} diff --git a/plugins/math/vec4/vec4.go b/plugins/math/vec4/vec4.go new file mode 100644 index 0000000..8ee8e8d --- /dev/null +++ b/plugins/math/vec4/vec4.go @@ -0,0 +1,144 @@ +package vec4 + +import ( + "fmt" + + "zworld/plugins/math" + "zworld/plugins/math/vec2" + "zworld/plugins/math/vec3" +) + +var ( + // Zero is the zero vector + Zero = T{0, 0, 0, 0} + + // One is the unit vector + One = T{1, 1, 1, 1} + + // UnitX returns a unit vector in the X direction + UnitX = T{1, 0, 0, 0} + + // UnitY returns a unit vector in the Y direction + UnitY = T{0, 1, 0, 0} + + // UnitZ returns a unit vector in the Z direction + UnitZ = T{0, 0, 1, 0} + + // UnitW returns a unit vector in the W direction + UnitW = T{0, 0, 0, 1} + + InfPos = T{math.InfPos, math.InfPos, math.InfPos, math.InfPos} + InfNeg = T{math.InfNeg, math.InfNeg, math.InfNeg, math.InfNeg} +) + +// T holds a 4-component vector of 32-bit floats +type T struct { + X, Y, Z, W float32 +} + +// Slice converts the vector into a 4-element slice of float32 +func (v T) Slice() [4]float32 { + return [4]float32{v.X, v.Y, v.Z, v.W} +} + +// Length returns the length of the vector. +// See also LengthSqr and Normalize. +func (v T) Length() float32 { + return math.Sqrt(v.LengthSqr()) +} + +// LengthSqr returns the squared length of the vector. +// See also Length and Normalize. +func (v T) LengthSqr() float32 { + return v.X*v.X + v.Y*v.Y + v.Z*v.Z + v.W*v.W +} + +// Abs sets every component of the vector to its absolute value. +func (v T) Abs() T { + return T{ + math.Abs(v.X), + math.Abs(v.Y), + math.Abs(v.Z), + math.Abs(v.W), + } +} + +// Normalize normalizes the vector to unit length. +func (v *T) Normalize() { + sl := v.LengthSqr() + if sl == 0 || sl == 1 { + return + } + s := 1 / math.Sqrt(sl) + v.X *= s + v.Y *= s + v.Z *= s + v.W *= s +} + +// Normalized returns a unit length normalized copy of the vector. +func (v T) Normalized() T { + v.Normalize() + return v +} + +// Scaled the vector +func (v T) Scaled(f float32) T { + return T{v.X * f, v.Y * f, v.Z * f, v.W * f} +} + +// Scale the vector by a constant (in-place) +func (v *T) Scale(f float32) { + v.X *= f + v.Y *= f + v.Z *= f + v.W *= f +} + +// Invert the vector components +func (v *T) Invert() { + v.X = -v.X + v.Y = -v.Y + v.Z = -v.Z + v.W = -v.W +} + +// Inverted returns an inverted vector +func (v T) Inverted() T { + v.Invert() + return v +} + +// Add each element of the vector with the corresponding element of another vector +func (v T) Add(v2 T) T { + return T{v.X + v2.X, v.Y + v2.Y, v.Z + v2.Z, v.W + v2.W} +} + +// Sub subtracts each element of the vector with the corresponding element of another vector +func (v T) Sub(v2 T) T { + return T{v.X - v2.X, v.Y - v2.Y, v.Z - v2.Z, v.W - v2.W} +} + +// Mul multiplies each element of the vector with the corresponding element of another vector +func (v T) Mul(v2 T) T { + return T{v.X * v2.X, v.Y * v2.Y, v.Z * v2.Z, v.W * v2.W} +} + +// XY returns a 2-component vector with the X, Y components +func (v T) XY() vec2.T { + return vec2.T{X: v.X, Y: v.Y} +} + +// XYZ returns a 3-component vector with the X, Y, Z components +func (v T) XYZ() vec3.T { + return vec3.T{X: v.X, Y: v.Y, Z: v.Z} +} + +// Div divides each element of the vector with the corresponding element of another vector +func (v T) Div(v2 T) T { + return T{v.X / v2.X, v.Y / v2.Y, v.Z / v2.Z, v.W / v2.W} +} + +func (v T) String() string { + return fmt.Sprintf("%.3f,%.3f,%.3f,%.3f", v.X, v.Y, v.Z, v.W) +} diff --git a/plugins/system/events/event.go b/plugins/system/events/event.go new file mode 100644 index 0000000..593401c --- /dev/null +++ b/plugins/system/events/event.go @@ -0,0 +1,30 @@ +package events + +type Data any + +type Handler[T Data] func(T) + +type Event[T Data] struct { + callbacks []Handler[T] +} + +func New[T Data]() Event[T] { + return Event[T]{} +} + +func (e Event[T]) Emit(event T) { + for _, callback := range e.callbacks { + if callback != nil { + callback(event) + } + } +} + +func (e *Event[T]) Subscribe(handler Handler[T]) func() { + id := len(e.callbacks) + e.callbacks = append(e.callbacks, handler) + return func() { + // unsub + e.callbacks[id] = nil + } +} diff --git a/plugins/system/input/debug.go b/plugins/system/input/debug.go new file mode 100644 index 0000000..fbb3b3c --- /dev/null +++ b/plugins/system/input/debug.go @@ -0,0 +1,39 @@ +package input + +import ( + "log" + + "zworld/plugins/system/input/keys" + "zworld/plugins/system/input/mouse" +) + +type nopHandler struct{} + +func (h *nopHandler) KeyEvent(e keys.Event) {} +func (h *nopHandler) MouseEvent(e mouse.Event) {} + +func NopHandler() Handler { + return &nopHandler{} +} + +type debugger struct { + Handler +} + +func DebugMiddleware(next Handler) Handler { + return &debugger{next} +} + +func (d debugger) KeyEvent(e keys.Event) { + log.Printf("%+v\n", e) + if d.Handler != nil { + d.Handler.KeyEvent(e) + } +} + +func (d debugger) MouseEvent(e mouse.Event) { + log.Printf("%+v\n", e) + if d.Handler != nil { + d.Handler.MouseEvent(e) + } +} diff --git a/plugins/system/input/handler.go b/plugins/system/input/handler.go new file mode 100644 index 0000000..d2e5a7d --- /dev/null +++ b/plugins/system/input/handler.go @@ -0,0 +1,19 @@ +package input + +import ( + "zworld/plugins/system/input/keys" + "zworld/plugins/system/input/mouse" +) + +type Handler interface { + KeyHandler + MouseHandler +} + +type KeyHandler interface { + KeyEvent(keys.Event) +} + +type MouseHandler interface { + MouseEvent(mouse.Event) +} diff --git a/plugins/system/input/keys/action.go b/plugins/system/input/keys/action.go new file mode 100644 index 0000000..bd89f64 --- /dev/null +++ b/plugins/system/input/keys/action.go @@ -0,0 +1,28 @@ +package keys + +import ( + "github.com/go-gl/glfw/v3.3/glfw" +) + +type Action glfw.Action + +const ( + Press Action = Action(glfw.Press) + Release = Action(glfw.Release) + Repeat = Action(glfw.Repeat) + Char = Action(3) +) + +func (a Action) String() string { + switch a { + case Press: + return "Press" + case Release: + return "Release" + case Repeat: + return "Repeat" + case Char: + return "Character" + } + return "Invalid" +} diff --git a/plugins/system/input/keys/event.go b/plugins/system/input/keys/event.go new file mode 100644 index 0000000..5fe486c --- /dev/null +++ b/plugins/system/input/keys/event.go @@ -0,0 +1,64 @@ +package keys + +import "fmt" + +type Event interface { + Code() Code + Action() Action + Character() rune + Modifier(Modifier) bool + + Handled() bool + Consume() +} + +type event struct { + handled bool + code Code + char rune + action Action + mods Modifier +} + +func (e event) Code() Code { return e.code } +func (e event) Character() rune { return e.char } +func (e event) Action() Action { return e.action } +func (e event) Handled() bool { return e.handled } + +func (e event) Modifier(mod Modifier) bool { + return e.mods&mod == mod +} + +func (e *event) Consume() { + e.handled = true +} + +func (e event) String() string { + switch e.action { + case Press: + return fmt.Sprintf("KeyEvent: %s %d %d", e.action, e.code, e.mods) + case Release: + return fmt.Sprintf("KeyEvent: %s %d %d", e.action, e.code, e.mods) + case Repeat: + return fmt.Sprintf("KeyEvent: %s %d %d", e.action, e.code, e.mods) + case Char: + return fmt.Sprintf("KeyEvent: %s %c", e.action, e.char) + } + return fmt.Sprintf("KeyEvent: Invalid Action %x", e.action) +} + +func NewCharEvent(char rune, mods Modifier) Event { + return &event{ + action: Char, + char: char, + mods: mods, + } +} + +func NewPressEvent(code Code, action Action, mods Modifier) Event { + return &event{ + code: code, + action: action, + mods: mods, + } +} diff --git a/plugins/system/input/keys/handler.go b/plugins/system/input/keys/handler.go new file mode 100644 index 0000000..c5235e1 --- /dev/null +++ b/plugins/system/input/keys/handler.go @@ -0,0 +1,70 @@ +package keys + +import ( + "github.com/go-gl/glfw/v3.3/glfw" +) + +type Callback func(Event) + +type Handler interface { + KeyEvent(Event) +} + +type FocusHandler interface { + Handler + FocusEvent() + BlurEvent() +} + +var focused FocusHandler + +func KeyCallbackWrapper(handler Handler) glfw.KeyCallback { + return func( + w *glfw.Window, + key glfw.Key, + scancode int, + action glfw.Action, + mods glfw.ModifierKey, + ) { + ev := &event{ + code: Code(key), + action: Action(action), + mods: Modifier(mods), + } + if focused != nil { + focused.KeyEvent(ev) + } else { + handler.KeyEvent(ev) + } + } +} + +func CharCallbackWrapper(handler Handler) glfw.CharCallback { + return func( + w *glfw.Window, + char rune, + ) { + ev := &event{ + char: char, + action: Char, + } + if focused != nil { + focused.KeyEvent(ev) + } else { + handler.KeyEvent(ev) + } + } +} + +func Focus(handler FocusHandler) { + if focused == handler { + return + } + if focused != nil { + focused.BlurEvent() + } + focused = handler + if focused != nil { + focused.FocusEvent() + } +} diff --git a/plugins/system/input/keys/keycodes.go b/plugins/system/input/keys/keycodes.go new file mode 100644 index 0000000..b625965 --- /dev/null +++ b/plugins/system/input/keys/keycodes.go @@ -0,0 +1,68 @@ +package keys + +import ( + "github.com/go-gl/glfw/v3.3/glfw" +) + +// Code represents a keyboard key +type Code glfw.Key + +// GLFW Keycodes +const ( + A Code = 65 + B Code = 66 + C Code = 67 + D Code = 68 + E Code = 69 + F Code = 70 + G Code = 71 + H Code = 72 + I Code = 73 + J Code = 74 + K Code = 75 + L Code = 76 + M Code = 77 + N Code = 78 + O Code = 79 + P Code = 80 + Q Code = 81 + R Code = 82 + S Code = 83 + T Code = 84 + U Code = 85 + V Code = 86 + W Code = 87 + X Code = 88 + Y Code = 89 + Z Code = 90 + + Key0 = Code(glfw.Key0) + Key1 = Code(glfw.Key1) + Key2 = Code(glfw.Key2) + Key3 = Code(glfw.Key3) + Key4 = Code(glfw.Key4) + Key5 = Code(glfw.Key5) + Key6 = Code(glfw.Key6) + Key7 = Code(glfw.Key7) + Key8 = Code(glfw.Key8) + Key9 = Code(glfw.Key9) + + Enter = Code(glfw.KeyEnter) + Escape = Code(glfw.KeyEscape) + Backspace = Code(glfw.KeyBackspace) + Delete = Code(glfw.KeyDelete) + Space = Code(glfw.KeySpace) + LeftShift = Code(glfw.KeyLeftShift) + RightShift = Code(glfw.KeyRightShift) + LeftControl = Code(glfw.KeyLeftControl) + RightControl = Code(glfw.KeyRightControl) + LeftAlt = Code(glfw.KeyLeftAlt) + RightAlt = Code(glfw.KeyRightAlt) + LeftSuper = Code(glfw.KeyLeftSuper) + RightSuper = Code(glfw.KeyRightSuper) + LeftArrow = Code(glfw.KeyLeft) + RightArrow = Code(glfw.KeyRight) + UpArrow = Code(glfw.KeyUp) + DownArrow = Code(glfw.KeyDown) + NumpadEnter = Code(glfw.KeyKPEnter) +) diff --git a/plugins/system/input/keys/modifier.go b/plugins/system/input/keys/modifier.go new file mode 100644 index 0000000..8144da8 --- /dev/null +++ b/plugins/system/input/keys/modifier.go @@ -0,0 +1,13 @@ +package keys + +import "github.com/go-gl/glfw/v3.3/glfw" + +type Modifier glfw.ModifierKey + +const ( + NoMod = Modifier(0) + Shift = Modifier(glfw.ModShift) + Ctrl = Modifier(glfw.ModControl) + Alt = Modifier(glfw.ModAlt) + Super = Modifier(glfw.ModSuper) +) diff --git a/plugins/system/input/keys/statemap.go b/plugins/system/input/keys/statemap.go new file mode 100644 index 0000000..5debe3d --- /dev/null +++ b/plugins/system/input/keys/statemap.go @@ -0,0 +1,55 @@ +package keys + +type State interface { + Handler + + Down(Code) bool + Up(Code) bool + + Shift() bool + Ctrl() bool + Alt() bool + Super() bool +} + +type state map[Code]bool + +func NewState() State { + return state{} +} + +func (s state) KeyEvent(e Event) { + if e.Action() == Press { + s[e.Code()] = true + } + if e.Action() == Release { + s[e.Code()] = false + } +} + +func (s state) Down(key Code) bool { + if state, stored := s[key]; stored { + return state + } + return false +} + +func (s state) Up(key Code) bool { + return !s.Down(key) +} + +func (s state) Shift() bool { + return s.Down(LeftShift) || s.Down(RightShift) +} + +func (s state) Alt() bool { + return s.Down(LeftAlt) || s.Down(RightAlt) +} + +func (s state) Ctrl() bool { + return s.Down(LeftControl) || s.Down(RightControl) +} + +func (s state) Super() bool { + return s.Down(LeftSuper) || s.Down(RightSuper) +} diff --git a/plugins/system/input/keys/util.go b/plugins/system/input/keys/util.go new file mode 100644 index 0000000..c58f62a --- /dev/null +++ b/plugins/system/input/keys/util.go @@ -0,0 +1,35 @@ +package keys + +func Pressed(ev Event, code Code) bool { + if ev.Action() != Press { + return false + } + if ev.Code() != code { + return false + } + return true +} + +func PressedMods(ev Event, code Code, mods Modifier) bool { + if !Pressed(ev, code) { + return false + } + return ev.Modifier(mods) +} + +func Released(ev Event, code Code) bool { + if ev.Action() != Release { + return false + } + if ev.Code() != code { + return false + } + return true +} + +func ReleasedMods(ev Event, code Code, mods Modifier) bool { + if !Released(ev, code) { + return false + } + return ev.Modifier(mods) +} diff --git a/plugins/system/input/mouse/action.go b/plugins/system/input/mouse/action.go new file mode 100644 index 0000000..d15b9b3 --- /dev/null +++ b/plugins/system/input/mouse/action.go @@ -0,0 +1,28 @@ +package mouse + +import "github.com/go-gl/glfw/v3.3/glfw" + +type Action int + +const ( + Press Action = Action(glfw.Press) + Release = Action(glfw.Release) + Move = Action(4) + Scroll = Action(5) + Enter = Action(6) + Leave = Action(7) +) + +func (a Action) String() string { + switch a { + case Press: + return "Press" + case Release: + return "Release" + case Move: + return "Move" + case Scroll: + return "Scroll" + } + return "Invalid" +} diff --git a/plugins/system/input/mouse/button.go b/plugins/system/input/mouse/button.go new file mode 100644 index 0000000..3ad322d --- /dev/null +++ b/plugins/system/input/mouse/button.go @@ -0,0 +1,21 @@ +package mouse + +import ( + "fmt" + + "github.com/go-gl/glfw/v3.3/glfw" +) + +type Button glfw.MouseButton + +const ( + Button1 Button = Button(glfw.MouseButton1) + Button2 = Button(glfw.MouseButton2) + Button3 = Button(glfw.MouseButton3) + Button4 = Button(glfw.MouseButton4) + Button5 = Button(glfw.MouseButton5) +) + +func (b Button) String() string { + return fmt.Sprintf("Button %d", int(b)+1) +} diff --git a/plugins/system/input/mouse/event.go b/plugins/system/input/mouse/event.go new file mode 100644 index 0000000..33d53b3 --- /dev/null +++ b/plugins/system/input/mouse/event.go @@ -0,0 +1,92 @@ +package mouse + +import ( + "fmt" + + "zworld/plugins/system/input/keys" + "zworld/plugins/math/vec2" +) + +type Event interface { + Action() Action + Button() Button + Position() vec2.T + Delta() vec2.T + Scroll() vec2.T + Modifier() keys.Modifier + Project(vec2.T) Event + Locked() bool + + Handled() bool + Consume() +} + +type event struct { + action Action + button Button + position vec2.T + delta vec2.T + scroll vec2.T + mods keys.Modifier + handled bool + locked bool +} + +func (e event) Action() Action { return e.action } +func (e event) Button() Button { return e.button } +func (e event) Position() vec2.T { return e.position } +func (e event) Delta() vec2.T { return e.delta } +func (e event) Scroll() vec2.T { return e.scroll } +func (e event) Modifier() keys.Modifier { return e.mods } +func (e event) Handled() bool { return e.handled } +func (e event) Locked() bool { return e.locked } + +func (e *event) Consume() { + e.handled = true +} + +func (e *event) Project(relativePos vec2.T) Event { + projected := *e + projected.position = projected.position.Sub(relativePos) + return &projected +} + +func (e event) String() string { + switch e.action { + case Move: + return fmt.Sprintf("MouseEvent: Moved to %.0f,%.0f (delta %.0f,%.0f)", + e.position.X, e.position.Y, + e.delta.X, e.delta.Y) + case Press: + return fmt.Sprintf("MouseEvent: Press %s at %.0f,%.0f", e.button, e.position.X, e.position.Y) + case Release: + return fmt.Sprintf("MouseEvent: Release %s at %.0f,%.0f", e.button, e.position.X, e.position.Y) + } + return "MouseEvent: Invalid" +} + +func NewButtonEvent(button Button, action Action, pos vec2.T, mod keys.Modifier, locked bool) Event { + return &event{ + action: action, + button: button, + mods: mod, + position: pos, + locked: locked, + } +} + +func NewMouseEnterEvent() Event { + return &event{ + action: Enter, + } +} + +func NewMouseLeaveEvent() Event { + return &event{ + action: Leave, + } +} + +func NopEvent() Event { + return &event{action: -1} +} diff --git a/plugins/system/input/mouse/handler.go b/plugins/system/input/mouse/handler.go new file mode 100644 index 0000000..962b629 --- /dev/null +++ b/plugins/system/input/mouse/handler.go @@ -0,0 +1,73 @@ +package mouse + +import ( + "github.com/go-gl/glfw/v3.3/glfw" + "zworld/plugins/system/input/keys" + "zworld/plugins/math/vec2" +) + +type Callback func(Event) + +type Handler interface { + MouseEvent(Event) +} + +type MouseWrapper interface { + Button(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) + Move(w *glfw.Window, x, y float64) + Scroll(w *glfw.Window, x, y float64) +} + +type wrapper struct { + Handler + position vec2.T +} + +func NewWrapper(handler Handler) MouseWrapper { + return &wrapper{ + Handler: handler, + } +} + +func (mw *wrapper) Button(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) { + mw.MouseEvent(&event{ + action: Action(action), + button: Button(button), + mods: keys.Modifier(mod), + position: mw.position, + locked: locked, + }) +} + +func (mw *wrapper) Move(w *glfw.Window, x, y float64) { + // calculate framebuffer scale relative to window + width, _ := w.GetSize() + fwidth, fheight := w.GetFramebufferSize() + scale := float32(fwidth) / float32(width) + + // calculate framebuffer position & mouse delta + pos := vec2.New(float32(x), float32(y)).Scaled(scale) + dt := pos.Sub(mw.position) + mw.position = pos + + // discard events that occur outside of the window bounds + if pos.X < 0 || pos.Y < 0 || int(pos.X) > fwidth || int(pos.Y) > fheight { + return + } + + // submit event to handler + mw.MouseEvent(&event{ + action: Move, + position: pos, + delta: dt, + locked: locked, + }) +} + +func (mw *wrapper) Scroll(w *glfw.Window, x, y float64) { + mw.MouseEvent(&event{ + action: Scroll, + scroll: vec2.New(float32(x), float32(y)), + locked: locked, + }) +} diff --git a/plugins/system/input/mouse/statemap.go b/plugins/system/input/mouse/statemap.go new file mode 100644 index 0000000..0a98952 --- /dev/null +++ b/plugins/system/input/mouse/statemap.go @@ -0,0 +1,34 @@ +package mouse + +type State interface { + Handler + + Down(Button) bool + Up(Button) bool +} + +type state map[Button]bool + +func NewState() State { + return state{} +} + +func (s state) MouseEvent(e Event) { + if e.Action() == Press { + s[e.Button()] = true + } + if e.Action() == Release { + s[e.Button()] = false + } +} + +func (s state) Down(key Button) bool { + if state, stored := s[key]; stored { + return state + } + return false +} + +func (s state) Up(key Button) bool { + return !s.Down(key) +} diff --git a/plugins/system/input/mouse/utils.go b/plugins/system/input/mouse/utils.go new file mode 100644 index 0000000..3d0d8b3 --- /dev/null +++ b/plugins/system/input/mouse/utils.go @@ -0,0 +1,25 @@ +package mouse + +import "github.com/go-gl/glfw/v3.3/glfw" + +var locked bool = false +var lockingEnabled bool = false + +func Lock() { + // actual cursor locking can be awkward, so leave an option to enable it + // otherwise the cursor will be locked virtually - i.e. only in the sense that + // mouse events have the Locked flag set to true + if lockingEnabled { + glfw.GetCurrentContext().SetInputMode(glfw.CursorMode, glfw.CursorDisabled) + } + locked = true +} + +func Hide() { + // glfw.GetCurrentContext().SetInputMode(glfw.CursorMode, glfw.CursorHidden) +} + +func Show() { + // glfw.GetCurrentContext().SetInputMode(glfw.CursorMode, glfw.CursorNormal) + locked = false +}