commit dc3b82de5e0b6f9cdce19b756298395c0359ed53 Author: ouczb Date: Sun Jan 14 22:56:06 2024 +0800 engine upload 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 0000000..9d09b0f Binary files /dev/null and b/assets/fonts/MaterialIcons-Regular.ttf differ diff --git a/assets/fonts/SourceCodeProRegular.ttf b/assets/fonts/SourceCodeProRegular.ttf new file mode 100644 index 0000000..437f472 Binary files /dev/null and b/assets/fonts/SourceCodeProRegular.ttf differ diff --git a/assets/fonts/SourceSansPro-Bold.ttf b/assets/fonts/SourceSansPro-Bold.ttf new file mode 100644 index 0000000..b8879af Binary files /dev/null and b/assets/fonts/SourceSansPro-Bold.ttf differ diff --git a/assets/fonts/SourceSansPro-Italic.ttf b/assets/fonts/SourceSansPro-Italic.ttf new file mode 100644 index 0000000..7dbece7 Binary files /dev/null and b/assets/fonts/SourceSansPro-Italic.ttf differ diff --git a/assets/fonts/SourceSansPro-Regular.ttf b/assets/fonts/SourceSansPro-Regular.ttf new file mode 100644 index 0000000..98e8579 Binary files /dev/null and b/assets/fonts/SourceSansPro-Regular.ttf differ diff --git a/assets/models/sphere.glb b/assets/models/sphere.glb new file mode 100644 index 0000000..fb6623e Binary files /dev/null and b/assets/models/sphere.glb differ diff --git a/assets/shaders/blur.fs.glsl b/assets/shaders/blur.fs.glsl new file mode 100644 index 0000000..85b0320 --- /dev/null +++ b/assets/shaders/blur.fs.glsl @@ -0,0 +1,24 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" + +IN(0, vec2, texcoord) +OUT(0, vec4, color) +SAMPLER(0, input) + +void main() +{ + vec2 texelSize = 1.0 / vec2(textureSize(tex_input, 0)); + float result = 0.0; + for (int x = -2; x < 2; ++x) + { + for (int y = -2; y < 2; ++y) + { + vec2 offset = vec2(float(x), float(y)) * texelSize; + result += texture(tex_input, in_texcoord + offset).r; + } + } + result = result / (4.0 * 4.0); + out_color = vec4(result, result, result, 1); +} diff --git a/assets/shaders/blur.json b/assets/shaders/blur.json new file mode 100644 index 0000000..49dc7ed --- /dev/null +++ b/assets/shaders/blur.json @@ -0,0 +1,15 @@ +{ + "Inputs": { + "position": { + "Index": 0, + "Type": "float" + }, + "texcoord_0": { + "Index": 1, + "Type": "float" + } + }, + "Bindings": { + "Input": 0 + } +} diff --git a/assets/shaders/blur.vs.glsl b/assets/shaders/blur.vs.glsl new file mode 100644 index 0000000..e7103e4 --- /dev/null +++ b/assets/shaders/blur.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/deferred/color.fs.glsl b/assets/shaders/deferred/color.fs.glsl new file mode 100644 index 0000000..6e15b54 --- /dev/null +++ b/assets/shaders/deferred/color.fs.glsl @@ -0,0 +1,14 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" +#include "lib/deferred_fragment.glsl" + +STORAGE_BUFFER(1, Object, objects) + +void main() +{ + out_diffuse = in_color; + out_normal = pack_normal(in_normal); + out_position = vec4(in_position, 1); +} diff --git a/assets/shaders/deferred/color.json b/assets/shaders/deferred/color.json new file mode 100644 index 0000000..05fc349 --- /dev/null +++ b/assets/shaders/deferred/color.json @@ -0,0 +1,22 @@ +{ + "Inputs": { + "position": { + "Index": 0, + "Type": "float" + }, + "normal": { + "Index": 1, + "Type": "float" + }, + "color_0": { + "Index": 2, + "Type": "float" + } + }, + "Bindings": { + "Camera": 0, + "Objects": 1, + "Lights": 2, + "Textures": 3 + } +} diff --git a/assets/shaders/deferred/color.vs.glsl b/assets/shaders/deferred/color.vs.glsl new file mode 100644 index 0000000..d00fff3 --- /dev/null +++ b/assets/shaders/deferred/color.vs.glsl @@ -0,0 +1,31 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" +#include "lib/deferred_vertex.glsl" + +CAMERA(0, camera) +OBJECT_BUFFER(1, objects) + +// Attributes +IN(0, vec3, position) +IN(1, vec3, normal) +IN(2, vec4, color) + +void main() +{ + out_object = gl_InstanceIndex; + mat4 mv = camera.View * objects.item[out_object].model; + + // gbuffer diffuse + out_color = vec4(in_color, 1); + + // gbuffer position + out_position = (mv * vec4(in_position.xyz, 1.0)).xyz; + + // gbuffer view space normal + out_normal = normalize((mv * vec4(in_normal, 0.0)).xyz); + + // vertex clip space position + gl_Position = camera.Proj * vec4(out_position, 1); +} diff --git a/assets/shaders/deferred/textured.fs.glsl b/assets/shaders/deferred/textured.fs.glsl new file mode 100644 index 0000000..626565f --- /dev/null +++ b/assets/shaders/deferred/textured.fs.glsl @@ -0,0 +1,18 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" +#include "lib/deferred_fragment.glsl" + +STORAGE_BUFFER(1, Object, objects) +SAMPLER_ARRAY(3, textures) + +void main() +{ + vec2 texcoord0 = in_color.xy; + uint texture0 = objects.item[in_object].textures[0]; + + out_diffuse = vec4(texture(textures[texture0], texcoord0).rgb, 1); + out_normal = pack_normal(in_normal); + out_position = vec4(in_position, 1); +} diff --git a/assets/shaders/deferred/textured.json b/assets/shaders/deferred/textured.json new file mode 100644 index 0000000..db05275 --- /dev/null +++ b/assets/shaders/deferred/textured.json @@ -0,0 +1,23 @@ +{ + "Inputs": { + "position": { + "Index": 0, + "Type": "float" + }, + "normal": { + "Index": 1, + "Type": "float" + }, + "texcoord_0": { + "Index": 2, + "Type": "float" + } + }, + "Bindings": { + "Camera": 0, + "Objects": 1, + "Lights": 2, + "Textures": 3 + }, + "Textures": ["diffuse"] +} diff --git a/assets/shaders/deferred/textured.vs.glsl b/assets/shaders/deferred/textured.vs.glsl new file mode 100644 index 0000000..37d7d2f --- /dev/null +++ b/assets/shaders/deferred/textured.vs.glsl @@ -0,0 +1,31 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" +#include "lib/deferred_vertex.glsl" + +CAMERA(0, camera) +STORAGE_BUFFER(1, Object, objects) + +// Attributes +IN(0, vec3, position) +IN(1, vec3, normal) +IN(2, vec2, texcoord) + +void main() +{ + out_object = gl_InstanceIndex; + mat4 mv = camera.View * objects.item[out_object].model; + + // textures + out_color.xy = in_texcoord; + + // gbuffer position + out_position = (mv * vec4(in_position, 1)).xyz; + + // gbuffer normal + out_normal = normalize((mv * vec4(in_normal, 0.0)).xyz); + + // vertex clip space position + gl_Position = camera.Proj * vec4(out_position, 1); +} diff --git a/assets/shaders/depth.fs.glsl b/assets/shaders/depth.fs.glsl new file mode 100644 index 0000000..644fedb --- /dev/null +++ b/assets/shaders/depth.fs.glsl @@ -0,0 +1,15 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" + +IN(0, vec3, normal) +IN(1, vec3, position) +OUT(0, vec4, normal) +OUT(1, vec4, position) + +void main() +{ + out_normal = pack_normal(in_normal); + out_position = vec4(in_position, 1); +} diff --git a/assets/shaders/depth.json b/assets/shaders/depth.json new file mode 100644 index 0000000..4a592d7 --- /dev/null +++ b/assets/shaders/depth.json @@ -0,0 +1,18 @@ +{ + "Inputs": { + "position": { + "Index": 0, + "Type": "float" + }, + "normal": { + "Index": 1, + "Type": "float" + } + }, + "Bindings": { + "Camera": 0, + "Objects": 1, + "Lights": 2, + "Textures": 3 + } +} diff --git a/assets/shaders/depth.vs.glsl b/assets/shaders/depth.vs.glsl new file mode 100644 index 0000000..d313500 --- /dev/null +++ b/assets/shaders/depth.vs.glsl @@ -0,0 +1,35 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" + +CAMERA(0, camera) +STORAGE_BUFFER(1, Object, objects) + +// Attributes +layout (location = 0) in vec3 position; +layout (location = 1) in vec3 normal; + +// Varyings +layout (location = 0) out vec3 normal0; +layout (location = 1) out vec3 position0; + +out gl_PerVertex +{ + vec4 gl_Position; +}; + +void main() +{ + mat4 m = objects.item[gl_InstanceIndex].model; + mat4 mv = camera.View * m; + + // gbuffer view position + position0 = (mv * vec4(position.xyz, 1.0)).xyz; + + // gbuffer view space normal + normal0 = normalize((mv * vec4(normal, 0.0)).xyz); + + // vertex clip space position + gl_Position = camera.Proj * vec4(position0, 1); +} diff --git a/assets/shaders/forward/color.fs.glsl b/assets/shaders/forward/color.fs.glsl new file mode 100644 index 0000000..c9df73d --- /dev/null +++ b/assets/shaders/forward/color.fs.glsl @@ -0,0 +1,24 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" +#include "lib/lighting.glsl" +#include "lib/forward_fragment.glsl" + +CAMERA(0, camera) +STORAGE_BUFFER(1, Object, objects) +LIGHT_BUFFER(2, lights) +SAMPLER_ARRAY(3, textures) + +void main() +{ + int lightCount = lights.settings.Count; + vec3 lightColor = ambientLight(lights.settings, 1); + for(int i = 0; i < lightCount; i++) { + lightColor += calculateLightColor(lights.item[i], in_world_position, in_world_normal, in_view_position.z, lights.settings); + } + + // gamma correct & write fragment + vec3 linearColor = pow(in_color.rgb, vec3(gamma)); + out_diffuse = vec4(linearColor * lightColor, in_color.a); +} diff --git a/assets/shaders/forward/color.json b/assets/shaders/forward/color.json new file mode 100644 index 0000000..05fc349 --- /dev/null +++ b/assets/shaders/forward/color.json @@ -0,0 +1,22 @@ +{ + "Inputs": { + "position": { + "Index": 0, + "Type": "float" + }, + "normal": { + "Index": 1, + "Type": "float" + }, + "color_0": { + "Index": 2, + "Type": "float" + } + }, + "Bindings": { + "Camera": 0, + "Objects": 1, + "Lights": 2, + "Textures": 3 + } +} diff --git a/assets/shaders/forward/color.vs.glsl b/assets/shaders/forward/color.vs.glsl new file mode 100644 index 0000000..d24d24c --- /dev/null +++ b/assets/shaders/forward/color.vs.glsl @@ -0,0 +1,33 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" +#include "lib/forward_vertex.glsl" + +CAMERA(0, camera) +STORAGE_BUFFER(1, Object, objects) + +// Attributes +IN(0, vec3, position) +IN(1, vec3, normal) +IN(2, vec4, color) + +void main() +{ + out_object = gl_InstanceIndex; + mat4 m = objects.item[out_object].model; + mat4 mv = camera.View * m; + + // gbuffer diffuse + out_color = in_color.rgba; + + // gbuffer view position + out_view_position = (mv * vec4(in_position.xyz, 1.0)).xyz; + out_world_position = (m * vec4(in_position.xyz, 1.0)).xyz; + + // world normal + out_world_normal = normalize((m * vec4(in_normal, 0.0)).xyz); + + // vertex clip space position + gl_Position = camera.Proj * vec4(out_view_position, 1); +} diff --git a/assets/shaders/forward/skybox.fs.glsl b/assets/shaders/forward/skybox.fs.glsl new file mode 100644 index 0000000..5e548d8 --- /dev/null +++ b/assets/shaders/forward/skybox.fs.glsl @@ -0,0 +1,37 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" +#include "lib/lighting.glsl" +#include "lib/forward_fragment.glsl" + +CAMERA(0, camera) +STORAGE_BUFFER(1, Object, objects) +LIGHT_BUFFER(2, lights) +SAMPLER_ARRAY(3, textures) + +float arc(vec3 a, vec3 b, float threshold) { + return max(0, dot(a, b) - threshold) / (1 - threshold); +} + +void main() +{ + vec3 sky_angle = normalize(in_world_position - camera.Eye.xyz); + + vec3 sun_angle = normalize(vec3(0, 1, -1)); + vec3 sun_color = vec3(1, 0.8, 0.4); + float sun_intensity = 16; + float sun_halo = sun_intensity * 0.007; + + vec3 top = vec3(0.53, 0.69, 0.85); + vec3 horizon = vec3(0.06, 0.18, 0.49); + + float frac = max(0, dot(sky_angle, vec3(0,1,0))); + + vec3 sky_color = mix(top, horizon, frac) + + asin(arc(sun_angle, sky_angle, 0.9985)) * sun_intensity * sun_color + + asin(arc(sun_angle, sky_angle, 0.99)) * sun_halo * sun_color; + + out_diffuse = vec4(sky_color, in_color.a); +} + diff --git a/assets/shaders/forward/skybox.json b/assets/shaders/forward/skybox.json new file mode 100644 index 0000000..62468e2 --- /dev/null +++ b/assets/shaders/forward/skybox.json @@ -0,0 +1,22 @@ +{ + "Inputs": { + "position": { + "Index": 0, + "Type": "float" + }, + "normal": { + "Index": 1, + "Type": "float" + }, + "texcoord_0": { + "Index": 2, + "Type": "float" + } + }, + "Bindings": { + "Camera": 0, + "Objects": 1, + "Lights": 2, + "Textures": 3 + } +} diff --git a/assets/shaders/forward/skybox.vs.glsl b/assets/shaders/forward/skybox.vs.glsl new file mode 100644 index 0000000..d24d24c --- /dev/null +++ b/assets/shaders/forward/skybox.vs.glsl @@ -0,0 +1,33 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" +#include "lib/forward_vertex.glsl" + +CAMERA(0, camera) +STORAGE_BUFFER(1, Object, objects) + +// Attributes +IN(0, vec3, position) +IN(1, vec3, normal) +IN(2, vec4, color) + +void main() +{ + out_object = gl_InstanceIndex; + mat4 m = objects.item[out_object].model; + mat4 mv = camera.View * m; + + // gbuffer diffuse + out_color = in_color.rgba; + + // gbuffer view position + out_view_position = (mv * vec4(in_position.xyz, 1.0)).xyz; + out_world_position = (m * vec4(in_position.xyz, 1.0)).xyz; + + // world normal + out_world_normal = normalize((m * vec4(in_normal, 0.0)).xyz); + + // vertex clip space position + gl_Position = camera.Proj * vec4(out_view_position, 1); +} diff --git a/assets/shaders/forward/sprite.fs.glsl b/assets/shaders/forward/sprite.fs.glsl new file mode 100644 index 0000000..e6ba5a6 --- /dev/null +++ b/assets/shaders/forward/sprite.fs.glsl @@ -0,0 +1,24 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" +#include "lib/forward_fragment.glsl" + +CAMERA(0, camera) +STORAGE_BUFFER(1, Object, objects) +SAMPLER_ARRAY(3, textures) + +void main() +{ + vec2 texcoord0 = in_color.xy; + uint texture0 = objects.item[in_object].textures[0]; + vec4 albedo = texture(textures[texture0], texcoord0); + + if (albedo.a < 0.5) { + discard; + } + + // gamma correct & write fragment + vec3 linearColor = pow(albedo.rgb, vec3(gamma)); + out_diffuse = vec4(linearColor, albedo.a); +} diff --git a/assets/shaders/forward/sprite.json b/assets/shaders/forward/sprite.json new file mode 100644 index 0000000..db05275 --- /dev/null +++ b/assets/shaders/forward/sprite.json @@ -0,0 +1,23 @@ +{ + "Inputs": { + "position": { + "Index": 0, + "Type": "float" + }, + "normal": { + "Index": 1, + "Type": "float" + }, + "texcoord_0": { + "Index": 2, + "Type": "float" + } + }, + "Bindings": { + "Camera": 0, + "Objects": 1, + "Lights": 2, + "Textures": 3 + }, + "Textures": ["diffuse"] +} diff --git a/assets/shaders/forward/sprite.vs.glsl b/assets/shaders/forward/sprite.vs.glsl new file mode 100644 index 0000000..0c53a52 --- /dev/null +++ b/assets/shaders/forward/sprite.vs.glsl @@ -0,0 +1,42 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" +#include "lib/forward_vertex.glsl" + +CAMERA(0, camera) +STORAGE_BUFFER(1, Object, objects) + +// Attributes +IN(0, vec3, position) +IN(1, vec3, normal) +IN(2, vec2, texcoord) + +void main() +{ + out_object = gl_InstanceIndex; + mat4 m = objects.item[out_object].model; + + vec3 center = (m * vec4(0, 0, 0, 1.0)).xyz; + vec3 lookDirection = normalize(center - camera.Eye.xyz); + vec3 up = vec3(0, 1, 0); + + vec3 right = normalize(cross(up, lookDirection)); + up = normalize(cross(lookDirection, right)); + + out_world_position = center + + right * in_position.x + + up * in_position.y; + + // texture coords + out_color.xy = in_texcoord; + + // gbuffer view position + out_view_position = (camera.View * vec4(out_world_position.xyz, 1.0)).xyz; + + // world normal is always facing the camera + out_world_normal = normalize(center - camera.Eye.xyz); + + // vertex clip space position + gl_Position = camera.Proj * vec4(out_view_position, 1); +} diff --git a/assets/shaders/forward/textured.fs.glsl b/assets/shaders/forward/textured.fs.glsl new file mode 100644 index 0000000..62f5743 --- /dev/null +++ b/assets/shaders/forward/textured.fs.glsl @@ -0,0 +1,28 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" +#include "lib/lighting.glsl" +#include "lib/forward_fragment.glsl" + +CAMERA(0, camera) +STORAGE_BUFFER(1, Object, objects) +LIGHT_BUFFER(2, lights) +SAMPLER_ARRAY(3, textures) + +void main() +{ + vec2 texcoord0 = in_color.xy; + uint texture0 = objects.item[in_object].textures[0]; + vec4 albedo = texture(textures[texture0], texcoord0); + + int lightCount = lights.settings.Count; + vec3 lightColor = ambientLight(lights.settings, 1); + for(int i = 0; i < lightCount; i++) { + lightColor += calculateLightColor(lights.item[i], in_world_position, in_world_normal, in_view_position.z, lights.settings); + } + + // gamma correct & write fragment + vec3 linearColor = pow(albedo.rgb, vec3(gamma)); + out_diffuse = vec4(linearColor * lightColor, albedo.a); +} diff --git a/assets/shaders/forward/textured.json b/assets/shaders/forward/textured.json new file mode 100644 index 0000000..db05275 --- /dev/null +++ b/assets/shaders/forward/textured.json @@ -0,0 +1,23 @@ +{ + "Inputs": { + "position": { + "Index": 0, + "Type": "float" + }, + "normal": { + "Index": 1, + "Type": "float" + }, + "texcoord_0": { + "Index": 2, + "Type": "float" + } + }, + "Bindings": { + "Camera": 0, + "Objects": 1, + "Lights": 2, + "Textures": 3 + }, + "Textures": ["diffuse"] +} diff --git a/assets/shaders/forward/textured.vs.glsl b/assets/shaders/forward/textured.vs.glsl new file mode 100644 index 0000000..8e5c70c --- /dev/null +++ b/assets/shaders/forward/textured.vs.glsl @@ -0,0 +1,33 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" +#include "lib/forward_vertex.glsl" + +CAMERA(0, camera) +STORAGE_BUFFER(1, Object, objects) + +// Attributes +IN(0, vec3, position) +IN(1, vec3, normal) +IN(2, vec2, texcoord) + +void main() +{ + out_object = gl_InstanceIndex; + mat4 m = objects.item[out_object].model; + mat4 mv = camera.View * m; + + // texture coords + out_color.xy = in_texcoord; + + // gbuffer view position + out_view_position = (mv * vec4(in_position.xyz, 1.0)).xyz; + out_world_position = (m * vec4(in_position.xyz, 1.0)).xyz; + + // world normal + out_world_normal = normalize((m * vec4(in_normal, 0.0)).xyz); + + // vertex clip space position + gl_Position = camera.Proj * vec4(out_view_position, 1); +} diff --git a/assets/shaders/game/voxels.fs.glsl b/assets/shaders/game/voxels.fs.glsl new file mode 100644 index 0000000..247fcc3 --- /dev/null +++ b/assets/shaders/game/voxels.fs.glsl @@ -0,0 +1,12 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" +#include "lib/deferred_fragment.glsl" + +void main() +{ + out_diffuse = vec4(in_color.rgb * in_color.a, 1); + out_normal = pack_normal(in_normal); + out_position = vec4(in_position, 1); +} diff --git a/assets/shaders/game/voxels.json b/assets/shaders/game/voxels.json new file mode 100644 index 0000000..2bf1cfa --- /dev/null +++ b/assets/shaders/game/voxels.json @@ -0,0 +1,26 @@ +{ + "Inputs": { + "position": { + "Index": 0, + "Type": "float" + }, + "normal_id": { + "Index": 1, + "Type": "uint8" + }, + "color_0": { + "Index": 2, + "Type": "float" + }, + "occlusion": { + "Index": 3, + "Type": "float" + } + }, + "Bindings": { + "Camera": 0, + "Objects": 1, + "Lights": 2, + "Textures": 3 + } +} diff --git a/assets/shaders/game/voxels.vs.glsl b/assets/shaders/game/voxels.vs.glsl new file mode 100644 index 0000000..a2a9bea --- /dev/null +++ b/assets/shaders/game/voxels.vs.glsl @@ -0,0 +1,43 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +#include "lib/common.glsl" +#include "lib/deferred_vertex.glsl" + +CAMERA(0, camera) +STORAGE_BUFFER(1, Object, objects) + +// Attributes +IN(0, vec3, position) +IN(1, uint, normal_id) +IN(2, vec3, color) +IN(3, float, occlusion) + +// normal lookup table +const vec3 normals[7] = vec3[7] ( + vec3(0,0,0), // normal 0 - undefined + vec3(1,0,0), // x+ + vec3(-1,0,0), // x- + vec3(0,1,0), // y+ + vec3(0,-1,0), // y- + vec3(0,0,1), // z+ + vec3(0,0,-1) // z- +); + +void main() +{ + mat4 mv = camera.View * objects.item[gl_InstanceIndex].model; + + // gbuffer diffuse + out_color = vec4(in_color, 1 - in_occlusion); + + // gbuffer view space position + out_position = (mv * vec4(in_position.xyz, 1.0)).xyz; + + // gbuffer view space normal + vec3 normal = normals[in_normal_id]; + out_normal = normalize((mv * vec4(normal, 0.0)).xyz); + + // vertex clip space position + gl_Position = camera.Proj * vec4(out_position, 1); +} diff --git a/assets/shaders/lib/common.glsl b/assets/shaders/lib/common.glsl new file mode 100644 index 0000000..88b9186 --- /dev/null +++ b/assets/shaders/lib/common.glsl @@ -0,0 +1,44 @@ +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable +#extension GL_EXT_nonuniform_qualifier : enable + +const float gamma = 2.2; + +struct Object { + mat4 model; + uint textures[4]; +}; + +#define SAMPLER_ARRAY(idx,name) \ + layout (binding = idx) uniform sampler2D[] name; \ + float _shadow_texture(uint index, vec2 point) { return texture(name[index], point).r; } \ + vec2 _shadow_size(uint index) { return textureSize(name[index], 0).xy; } + +#define SAMPLER(idx,name) layout (binding = idx) uniform sampler2D tex_ ## name; + +#define UNIFORM(idx,name,body) layout (binding = idx) uniform uniform_ ## name body name; + +#define STORAGE_BUFFER(idx,type,name) layout (binding = idx) readonly buffer uniform_ ## name { type item[]; } name; + +#define CAMERA(idx,name) layout (binding = idx) uniform Camera { \ + mat4 Proj; \ + mat4 View; \ + mat4 ViewProj; \ + mat4 ProjInv; \ + mat4 ViewInv; \ + mat4 ViewProjInv; \ + vec4 Eye; \ + vec4 Forward; \ + vec2 Viewport; \ +} name; + +#define IN(idx,type,name) layout (location = idx) in type in_ ## name; +#define OUT(idx,type,name) layout (location = idx) out type out_ ## name; + +vec3 unpack_normal(vec3 packed_normal) { + return normalize(2.0 * packed_normal - 1); +} + +vec4 pack_normal(vec3 normal) { + return vec4((normal + 1.0) / 2.0, 1); +} diff --git a/assets/shaders/lib/deferred_fragment.glsl b/assets/shaders/lib/deferred_fragment.glsl new file mode 100644 index 0000000..f039b39 --- /dev/null +++ b/assets/shaders/lib/deferred_fragment.glsl @@ -0,0 +1,10 @@ +// Varying +IN(0, flat uint, object) +IN(1, vec3, normal) +IN(2, vec3, position) +IN(3, vec4, color) + +// Return Output +OUT(0, vec4, diffuse) +OUT(1, vec4, normal) +OUT(2, vec4, position) diff --git a/assets/shaders/lib/deferred_vertex.glsl b/assets/shaders/lib/deferred_vertex.glsl new file mode 100644 index 0000000..bef90c1 --- /dev/null +++ b/assets/shaders/lib/deferred_vertex.glsl @@ -0,0 +1,11 @@ +// Common vertex shader code +// Varyings +OUT(0, flat uint, object) +OUT(1, vec3, normal) +OUT(2, vec3, position) +OUT(3, vec4, color) + +out gl_PerVertex +{ + vec4 gl_Position; +}; diff --git a/assets/shaders/lib/forward_fragment.glsl b/assets/shaders/lib/forward_fragment.glsl new file mode 100644 index 0000000..e4d3a9a --- /dev/null +++ b/assets/shaders/lib/forward_fragment.glsl @@ -0,0 +1,8 @@ +IN(0, flat uint, object) +IN(1, vec4, color) +IN(2, vec3, view_position) +IN(3, vec3, world_normal) +IN(4, vec3, world_position) + +// Return Output +OUT(0, vec4, diffuse) diff --git a/assets/shaders/lib/forward_vertex.glsl b/assets/shaders/lib/forward_vertex.glsl new file mode 100644 index 0000000..d612f1b --- /dev/null +++ b/assets/shaders/lib/forward_vertex.glsl @@ -0,0 +1,10 @@ +OUT(0, flat uint, object) +OUT(1, vec4, color) +OUT(2, vec3, view_position) +OUT(3, vec3, world_normal) +OUT(4, vec3, world_position) + +out gl_PerVertex +{ + vec4 gl_Position; +}; diff --git a/assets/shaders/lib/lighting.glsl b/assets/shaders/lib/lighting.glsl new file mode 100644 index 0000000..f8a11a7 --- /dev/null +++ b/assets/shaders/lib/lighting.glsl @@ -0,0 +1,214 @@ +#define POINT_LIGHT 1 +#define DIRECTIONAL_LIGHT 2 + +#define SHADOW_CASCADES 4 + +struct Light { + mat4 ViewProj[SHADOW_CASCADES]; + int Shadowmap[SHADOW_CASCADES]; + float Distance[SHADOW_CASCADES]; + + vec4 Color; + vec4 Position; + uint Type; + float Intensity; + float Range; + float Falloff; + + float _padding; +}; + +struct LightSettings { + vec4 AmbientColor; + float AmbientIntensity; + int Count; + int ShadowSamples; + float ShadowSampleRadius; + float ShadowBias; + float NormalOffset; +}; + +#define LIGHT_PADDING 75 +#define LIGHT_BUFFER(idx,name) layout (binding = idx) readonly buffer LightBuffer { LightSettings settings; float[LIGHT_PADDING] _padding; Light item[]; } name; + +const float SHADOW_POWER = 60; + +// transforms ndc -> 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 0000000..b4a61ed Binary files /dev/null and b/assets/textures/color_grading/none.png differ diff --git a/assets/textures/fire.png b/assets/textures/fire.png new file mode 100644 index 0000000..de1c797 Binary files /dev/null and b/assets/textures/fire.png differ diff --git a/assets/textures/heightmap.png b/assets/textures/heightmap.png new file mode 100644 index 0000000..9991a74 Binary files /dev/null and b/assets/textures/heightmap.png differ diff --git a/assets/textures/palette.png b/assets/textures/palette.png new file mode 100644 index 0000000..77aead9 Binary files /dev/null and b/assets/textures/palette.png differ diff --git a/assets/textures/shit_logo.png b/assets/textures/shit_logo.png new file mode 100644 index 0000000..047486d Binary files /dev/null and b/assets/textures/shit_logo.png differ diff --git a/assets/textures/ui/light.png b/assets/textures/ui/light.png new file mode 100644 index 0000000..b3d4f28 Binary files /dev/null and b/assets/textures/ui/light.png differ diff --git a/assets/textures/uv_checker.png b/assets/textures/uv_checker.png new file mode 100644 index 0000000..0fdaba8 Binary files /dev/null and b/assets/textures/uv_checker.png differ diff --git a/engine/frame.go b/engine/frame.go new file mode 100644 index 0000000..473e8d6 --- /dev/null +++ b/engine/frame.go @@ -0,0 +1,45 @@ +package engine + +import ( + osimage "image" + "runtime" + "zworld/engine/object" + "zworld/engine/render/graph" + "zworld/engine/renderapi/image" + "zworld/engine/renderapi/vulkan" +) + +// Render a single frame and return it as *image.RGBA +func Frame(args Args, scenefuncs ...SceneFunc) *osimage.RGBA { + runtime.LockOSThread() + + backend := vulkan.New("goworld", 0) + defer backend.Destroy() + + if args.Renderer == nil { + args.Renderer = graph.Default + } + + buffer := vulkan.NewColorTarget(backend.Device(), "output", image.FormatRGBA8Unorm, vulkan.TargetSize{ + Width: args.Width, + Height: args.Height, + Frames: 1, + Scale: 1, + }) + defer buffer.Destroy() + + // create renderer + renderer := args.Renderer(backend, buffer) + defer renderer.Destroy() + + // create scene + scene := object.Empty("Scene") + for _, scenefunc := range scenefuncs { + scenefunc(scene) + } + + scene.Update(scene, 0) + renderer.Draw(scene, 0, 0) + + return renderer.Screengrab() +} diff --git a/engine/frame_counter.go b/engine/frame_counter.go new file mode 100644 index 0000000..45c0fd5 --- /dev/null +++ b/engine/frame_counter.go @@ -0,0 +1,76 @@ +package engine + +import ( + "time" + "zworld/plugins/math" +) + +type framecounter struct { + next int + samples int + last int64 + frames []int64 + start time.Time + now time.Time + elapsed float32 + delta float32 +} + +func NewFrameCounter(samples int) *framecounter { + return &framecounter{ + samples: samples, + last: time.Now().UnixNano(), + frames: make([]int64, 0, samples), + start: time.Now(), + now: time.Now(), + } +} + +type Timing struct { + Current float32 + Average float32 + Max float32 +} + +func (fc *framecounter) Elapsed() float32 { + return fc.elapsed +} + +func (fc *framecounter) Delta() float32 { + return fc.delta +} + +func (fc *framecounter) Update() { + // clock + now := time.Now() + fc.delta = float32(now.Sub(fc.now).Seconds()) + fc.now = now + fc.elapsed = float32(fc.now.Sub(fc.start).Seconds()) + + // fps + ft := fc.now.UnixNano() + ns := ft - fc.last + fc.last = ft + if len(fc.frames) < fc.samples { + fc.frames = append(fc.frames, ns) + } else { + fc.frames[fc.next%fc.samples] = ns + } + fc.next++ +} + +func (fc *framecounter) Sample() Timing { + tot := int64(0) + max := int64(0) + for _, f := range fc.frames { + tot += f + max = math.Max(max, f) + } + + current := fc.frames[(fc.next-1)%fc.samples] + return Timing{ + Average: float32(tot) / float32(len(fc.frames)) / 1e9, + Max: float32(max) / 1e9, + Current: float32(current) / 1e9, + } +} diff --git a/engine/interrupter.go b/engine/interrupter.go new file mode 100644 index 0000000..e0f0462 --- /dev/null +++ b/engine/interrupter.go @@ -0,0 +1,39 @@ +package engine + +import ( + "log" + "os" + "os/signal" +) + +type Interrupter interface { + Running() bool +} + +type interrupter struct { + running bool +} + +func (r *interrupter) Running() bool { + return r.running +} + +func NewInterrupter() Interrupter { + sigint := make(chan os.Signal, 1) + signal.Notify(sigint, os.Interrupt) + r := &interrupter{running: true} + + go func() { + for range sigint { + if !r.running { + log.Println("Kill") + os.Exit(1) + } else { + log.Println("Interrupt") + r.running = false + } + } + }() + + return r +} diff --git a/engine/object/actor.go b/engine/object/actor.go new file mode 100644 index 0000000..0d189df --- /dev/null +++ b/engine/object/actor.go @@ -0,0 +1,228 @@ +package object + +import ( + "reflect" + "zworld/plugins/math/transform" + "zworld/plugins/system/input" + "zworld/plugins/system/input/keys" + "zworld/plugins/system/input/mouse" +) + +type Object interface { + Component + input.Handler + + // Children returns a slice containing the objects children. + Children() []Component + + attach(...Component) + detach(Component) +} + +// objectType caches a reference to Object's reflect.Type +var objectType = reflect.TypeOf((*Object)(nil)).Elem() + +type object struct { + component + transform transform.T + children []Component +} + +func emptyObject(name string) object { + return object{ + component: emptyComponent(name), + transform: transform.Identity(), + } +} + +// Empty creates a new, empty object. +func Empty(name string) Object { + obj := emptyObject(name) + return &obj +} + +func New[K Object](name string, obj K) K { + t := reflect.TypeOf(obj).Elem() + v := reflect.ValueOf(obj).Elem() + + // find & initialize base object + 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 == objectType { + // the object directly extends the base object + // if its nil, create a new empty object base + if value.IsZero() { + base := Empty(name) + value.Set(reflect.ValueOf(base)) + } + } else if _, isObject := value.Interface().(Object); isObject { + // this object extends some other non-base object + } else { + // its not an object, move on + continue + } + + // if we already found a base field, the user has embedded multiple objects + if baseIdx >= 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 +}