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