engine upload
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
*.log
|
||||
.idea/*
|
||||
*/.ipynb_checkpoints/*
|
||||
53
assets/assets.go
Normal 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))
|
||||
}
|
||||
BIN
assets/fonts/MaterialIcons-Regular.ttf
Normal file
BIN
assets/fonts/SourceCodeProRegular.ttf
Normal file
BIN
assets/fonts/SourceSansPro-Bold.ttf
Normal file
BIN
assets/fonts/SourceSansPro-Italic.ttf
Normal file
BIN
assets/fonts/SourceSansPro-Regular.ttf
Normal file
BIN
assets/models/sphere.glb
Normal file
24
assets/shaders/blur.fs.glsl
Normal 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
@ -0,0 +1,15 @@
|
||||
{
|
||||
"Inputs": {
|
||||
"position": {
|
||||
"Index": 0,
|
||||
"Type": "float"
|
||||
},
|
||||
"texcoord_0": {
|
||||
"Index": 1,
|
||||
"Type": "float"
|
||||
}
|
||||
},
|
||||
"Bindings": {
|
||||
"Input": 0
|
||||
}
|
||||
}
|
||||
19
assets/shaders/blur.vs.glsl
Normal 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);
|
||||
}
|
||||
14
assets/shaders/deferred/color.fs.glsl
Normal 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);
|
||||
}
|
||||
22
assets/shaders/deferred/color.json
Normal 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
|
||||
}
|
||||
}
|
||||
31
assets/shaders/deferred/color.vs.glsl
Normal 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);
|
||||
}
|
||||
18
assets/shaders/deferred/textured.fs.glsl
Normal 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);
|
||||
}
|
||||
23
assets/shaders/deferred/textured.json
Normal 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"]
|
||||
}
|
||||
31
assets/shaders/deferred/textured.vs.glsl
Normal 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);
|
||||
}
|
||||
15
assets/shaders/depth.fs.glsl
Normal 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
@ -0,0 +1,18 @@
|
||||
{
|
||||
"Inputs": {
|
||||
"position": {
|
||||
"Index": 0,
|
||||
"Type": "float"
|
||||
},
|
||||
"normal": {
|
||||
"Index": 1,
|
||||
"Type": "float"
|
||||
}
|
||||
},
|
||||
"Bindings": {
|
||||
"Camera": 0,
|
||||
"Objects": 1,
|
||||
"Lights": 2,
|
||||
"Textures": 3
|
||||
}
|
||||
}
|
||||
35
assets/shaders/depth.vs.glsl
Normal 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);
|
||||
}
|
||||
24
assets/shaders/forward/color.fs.glsl
Normal 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);
|
||||
}
|
||||
22
assets/shaders/forward/color.json
Normal 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
|
||||
}
|
||||
}
|
||||
33
assets/shaders/forward/color.vs.glsl
Normal 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);
|
||||
}
|
||||
37
assets/shaders/forward/skybox.fs.glsl
Normal 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);
|
||||
}
|
||||
|
||||
22
assets/shaders/forward/skybox.json
Normal 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
|
||||
}
|
||||
}
|
||||
33
assets/shaders/forward/skybox.vs.glsl
Normal 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);
|
||||
}
|
||||
24
assets/shaders/forward/sprite.fs.glsl
Normal 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);
|
||||
}
|
||||
23
assets/shaders/forward/sprite.json
Normal 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"]
|
||||
}
|
||||
42
assets/shaders/forward/sprite.vs.glsl
Normal 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);
|
||||
}
|
||||
28
assets/shaders/forward/textured.fs.glsl
Normal 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);
|
||||
}
|
||||
23
assets/shaders/forward/textured.json
Normal 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"]
|
||||
}
|
||||
33
assets/shaders/forward/textured.vs.glsl
Normal 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);
|
||||
}
|
||||
12
assets/shaders/game/voxels.fs.glsl
Normal 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);
|
||||
}
|
||||
26
assets/shaders/game/voxels.json
Normal 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
|
||||
}
|
||||
}
|
||||
43
assets/shaders/game/voxels.vs.glsl
Normal 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);
|
||||
}
|
||||
44
assets/shaders/lib/common.glsl
Normal 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);
|
||||
}
|
||||
10
assets/shaders/lib/deferred_fragment.glsl
Normal 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)
|
||||
11
assets/shaders/lib/deferred_vertex.glsl
Normal 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;
|
||||
};
|
||||
8
assets/shaders/lib/forward_fragment.glsl
Normal 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)
|
||||
10
assets/shaders/lib/forward_vertex.glsl
Normal 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;
|
||||
};
|
||||
214
assets/shaders/lib/lighting.glsl
Normal 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;
|
||||
}
|
||||
23
assets/shaders/lib/ui.glsl
Normal 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;
|
||||
}
|
||||
60
assets/shaders/light.fs.glsl
Normal 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
@ -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
|
||||
}
|
||||
}
|
||||
19
assets/shaders/light.vs.glsl
Normal 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);
|
||||
}
|
||||
20
assets/shaders/lines.fs.glsl
Normal 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
@ -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
|
||||
}
|
||||
}
|
||||
24
assets/shaders/lines.vs.glsl
Normal 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);
|
||||
}
|
||||
11
assets/shaders/old/billboard.fs.glsl
Normal 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);
|
||||
}
|
||||
43
assets/shaders/old/billboard.gs.glsl
Normal 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();
|
||||
}
|
||||
10
assets/shaders/old/billboard.vs.glsl
Normal 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);
|
||||
}
|
||||
13
assets/shaders/output.fs.glsl
Normal 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);
|
||||
}
|
||||
15
assets/shaders/output.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"Inputs": {
|
||||
"position": {
|
||||
"Index": 0,
|
||||
"Type": "float"
|
||||
},
|
||||
"texcoord_0": {
|
||||
"Index": 1,
|
||||
"Type": "float"
|
||||
}
|
||||
},
|
||||
"Bindings": {
|
||||
"Output": 0
|
||||
}
|
||||
}
|
||||
19
assets/shaders/output.vs.glsl
Normal 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);
|
||||
}
|
||||
54
assets/shaders/postprocess.fs.glsl
Normal 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);
|
||||
}
|
||||
16
assets/shaders/postprocess.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"Inputs": {
|
||||
"position": {
|
||||
"Index": 0,
|
||||
"Type": "float"
|
||||
},
|
||||
"texcoord_0": {
|
||||
"Index": 1,
|
||||
"Type": "float"
|
||||
}
|
||||
},
|
||||
"Bindings": {
|
||||
"Input": 0,
|
||||
"LUT": 1
|
||||
}
|
||||
}
|
||||
19
assets/shaders/postprocess.vs.glsl
Normal 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);
|
||||
}
|
||||
13
assets/shaders/shadow.fs.glsl
Normal 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);
|
||||
}
|
||||
14
assets/shaders/shadow.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"Inputs": {
|
||||
"position": {
|
||||
"Index": 0,
|
||||
"Type": "float"
|
||||
}
|
||||
},
|
||||
"Bindings": {
|
||||
"Camera": 0,
|
||||
"Objects": 1,
|
||||
"Lights": 2,
|
||||
"Textures": 3
|
||||
}
|
||||
}
|
||||
25
assets/shaders/shadow.vs.glsl
Normal 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;
|
||||
}
|
||||
73
assets/shaders/ssao.fs.glsl
Normal 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
@ -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
|
||||
}
|
||||
}
|
||||
19
assets/shaders/ssao.vs.glsl
Normal 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);
|
||||
}
|
||||
56
assets/shaders/ui_quad.fs.glsl
Normal 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;
|
||||
}
|
||||
|
||||
13
assets/shaders/ui_quad.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"Inputs": {
|
||||
"position": {
|
||||
"Index": 0,
|
||||
"Type": "float"
|
||||
}
|
||||
},
|
||||
"Bindings": {
|
||||
"Config": 0,
|
||||
"Quads": 1,
|
||||
"Textures": 2
|
||||
}
|
||||
}
|
||||
47
assets/shaders/ui_quad.vs.glsl
Normal 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];
|
||||
}
|
||||
BIN
assets/textures/color_grading/none.png
Normal file
|
After Width: | Height: | Size: 646 B |
BIN
assets/textures/fire.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
assets/textures/heightmap.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
assets/textures/palette.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
assets/textures/shit_logo.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
assets/textures/ui/light.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
assets/textures/uv_checker.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
45
engine/frame.go
Normal 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
@ -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
@ -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
@ -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
@ -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
|
||||
}
|
||||
102
engine/object/camera/camera.go
Normal 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(),
|
||||
}
|
||||
}
|
||||
57
engine/object/camera/frustum.go
Normal 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
@ -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)
|
||||
}
|
||||
}
|
||||
236
engine/object/light/directional.go
Normal 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
|
||||
}
|
||||
37
engine/object/light/light.go
Normal 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
|
||||
}
|
||||
89
engine/object/light/point.go
Normal 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
|
||||
}
|
||||
12
engine/object/light/type.go
Normal 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
|
||||
)
|
||||
67
engine/object/mesh/dynamic.go
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1,11 @@
|
||||
package object
|
||||
|
||||
type EnableHandler interface {
|
||||
Component
|
||||
OnEnable()
|
||||
}
|
||||
|
||||
type DisableHandler interface {
|
||||
Component
|
||||
OnDisable()
|
||||
}
|
||||
19
engine/object/utils.go
Normal 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
@ -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)
|
||||
}
|
||||
}
|
||||
98
engine/render/graph/default.go
Normal 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,
|
||||
}
|
||||
})
|
||||
}
|
||||
174
engine/render/graph/graph.go
Normal 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
@ -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),
|
||||
})
|
||||
}
|
||||
44
engine/render/graph/post_node.go
Normal 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()
|
||||
}
|
||||
100
engine/render/graph/pre_node.go
Normal 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
|
||||
}
|
||||
66
engine/render/pass/basic_material.go
Normal 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()
|
||||
}
|
||||