engine upload

This commit is contained in:
ouczb 2024-01-14 22:56:06 +08:00
commit dc3b82de5e
303 changed files with 20327 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*.log
.idea/*
*/.ipynb_checkpoints/*

53
assets/assets.go Normal file
View File

@ -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))
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/models/sphere.glb Normal file

Binary file not shown.

View File

@ -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);
}

15
assets/shaders/blur.json Normal file
View File

@ -0,0 +1,15 @@
{
"Inputs": {
"position": {
"Index": 0,
"Type": "float"
},
"texcoord_0": {
"Index": 1,
"Type": "float"
}
},
"Bindings": {
"Input": 0
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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"]
}

View File

@ -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);
}

View File

@ -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);
}

18
assets/shaders/depth.json Normal file
View File

@ -0,0 +1,18 @@
{
"Inputs": {
"position": {
"Index": 0,
"Type": "float"
},
"normal": {
"Index": 1,
"Type": "float"
}
},
"Bindings": {
"Camera": 0,
"Objects": 1,
"Lights": 2,
"Textures": 3
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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"]
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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"]
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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)

View File

@ -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;
};

View File

@ -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)

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}

21
assets/shaders/light.json Normal file
View File

@ -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
}
}

View File

@ -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);
}

View File

@ -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);
}

18
assets/shaders/lines.json Normal file
View File

@ -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
}
}

View File

@ -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);
}

View File

@ -0,0 +1,11 @@
#version 330
uniform sampler2D sprite;
in vec2 uv;
layout(location=0) out vec4 color;
void main()
{
color = texture(sprite, uv);
}

View File

@ -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();
}

View File

@ -0,0 +1,10 @@
#version 330
layout (location=0) in vec3 position;
uniform mat4 model;
void main()
{
gl_Position = model * vec4(position, 1.0);
}

View File

@ -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);
}

View File

@ -0,0 +1,15 @@
{
"Inputs": {
"position": {
"Index": 0,
"Type": "float"
},
"texcoord_0": {
"Index": 1,
"Type": "float"
}
},
"Bindings": {
"Output": 0
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -0,0 +1,16 @@
{
"Inputs": {
"position": {
"Index": 0,
"Type": "float"
},
"texcoord_0": {
"Index": 1,
"Type": "float"
}
},
"Bindings": {
"Input": 0,
"LUT": 1
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -0,0 +1,14 @@
{
"Inputs": {
"position": {
"Index": 0,
"Type": "float"
}
},
"Bindings": {
"Camera": 0,
"Objects": 1,
"Lights": 2,
"Textures": 3
}
}

View File

@ -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;
}

View File

@ -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;
}

18
assets/shaders/ssao.json Normal file
View File

@ -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
}
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -0,0 +1,13 @@
{
"Inputs": {
"position": {
"Index": 0,
"Type": "float"
}
},
"Bindings": {
"Config": 0,
"Quads": 1,
"Textures": 2
}
}

View File

@ -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];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

BIN
assets/textures/fire.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
assets/textures/palette.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

45
engine/frame.go Normal file
View File

@ -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()
}

76
engine/frame_counter.go Normal file
View File

@ -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,
}
}

39
engine/interrupter.go Normal file
View File

@ -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
}

228
engine/object/actor.go Normal file
View File

@ -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)
}
}

99
engine/object/builder.go Normal file
View File

@ -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
}

View File

@ -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(),
}
}

View File

@ -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,
}
}

141
engine/object/component.go Normal file
View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
)

View File

@ -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:
}
}

149
engine/object/mesh/mesh.go Normal file
View File

@ -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
}

100
engine/object/property.go Normal file
View File

@ -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
}

119
engine/object/query.go Normal file
View File

@ -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)
}
}
}

300
engine/object/relations.go Normal file
View File

@ -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
}

195
engine/object/serialize.go Normal file
View File

@ -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
}

11
engine/object/type.go Normal file
View File

@ -0,0 +1,11 @@
package object
type EnableHandler interface {
Component
OnEnable()
}
type DisableHandler interface {
Component
OnDisable()
}

19
engine/object/utils.go Normal file
View File

@ -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))
}

16
engine/profiling.go Normal file
View File

@ -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)
}
}

View File

@ -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,
}
})
}

View File

@ -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]
}

185
engine/render/graph/node.go Normal file
View File

@ -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),
})
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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()
}

Some files were not shown because too many files have changed in this diff Show More