aboutsummaryrefslogtreecommitdiff
path: root/src/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/util')
-rw-r--r--src/util/geometry.c44
-rw-r--r--src/util/ibl.c328
-rw-r--r--src/util/shader.c136
-rw-r--r--src/util/skyquad.c161
4 files changed, 669 insertions, 0 deletions
diff --git a/src/util/geometry.c b/src/util/geometry.c
new file mode 100644
index 0000000..afe0109
--- /dev/null
+++ b/src/util/geometry.c
@@ -0,0 +1,44 @@
1#include <gfx/util/geometry.h>
2
3#include <math/vec2.h>
4
5static void make_quad_11_positions(vec2 positions[4]) {
6 positions[0] = vec2_make(-1, +1);
7 positions[1] = vec2_make(-1, -1);
8 positions[2] = vec2_make(+1, +1);
9 positions[3] = vec2_make(+1, -1);
10}
11
12static void make_quad_01_positions(vec2 positions[4]) {
13 positions[0] = vec2_make(0, 0);
14 positions[1] = vec2_make(1, 0);
15 positions[2] = vec2_make(1, 1);
16 positions[3] = vec2_make(0, 1);
17}
18
19static GeometryDesc make_quad_desc(vec2 positions[4]) {
20 GeometryDesc desc = (GeometryDesc){0};
21 desc.positions2d.data = positions;
22 desc.positions2d.size_bytes = 4 * sizeof(vec2);
23 desc.num_verts = 4;
24 desc.type = TriangleStrip;
25 return desc;
26}
27
28Geometry* gfx_make_quad_11(GfxCore* gfxcore) {
29 assert(gfxcore);
30
31 vec2 positions[4];
32 make_quad_11_positions(positions);
33 const GeometryDesc geometry_desc = make_quad_desc(positions);
34 return gfx_make_geometry(gfxcore, &geometry_desc);
35}
36
37Geometry* gfx_make_quad_01(GfxCore* gfxcore) {
38 assert(gfxcore);
39
40 vec2 positions[4];
41 make_quad_01_positions(positions);
42 const GeometryDesc geometry_desc = make_quad_desc(positions);
43 return gfx_make_geometry(gfxcore, &geometry_desc);
44}
diff --git a/src/util/ibl.c b/src/util/ibl.c
new file mode 100644
index 0000000..5a79990
--- /dev/null
+++ b/src/util/ibl.c
@@ -0,0 +1,328 @@
1#include <gfx/util/ibl.h>
2
3#include <gfx/core.h>
4#include <gfx/util/geometry.h>
5#include <gfx/util/shader.h>
6#include <math/mat4.h>
7
8#include <assert.h>
9#include <stdlib.h>
10
11typedef struct IBL {
12 Geometry* quad;
13 ShaderProgram* brdf_integration_map_shader;
14 ShaderProgram* irradiance_map_shader;
15 ShaderProgram* prefiltered_environment_map_shader;
16 Texture* brdf_integration_map;
17 FrameBuffer* framebuffer;
18 mat4 rotations[6];
19} IBL;
20
21static const CubemapFace faces[6] = {
22 CubemapFacePosX, // Right.
23 CubemapFaceNegX, // Left.
24 CubemapFacePosY, // Up.
25 CubemapFaceNegY, // Down.
26 CubemapFacePosZ, // Back.
27 CubemapFaceNegZ, // Front.
28};
29
30static const float flips[6] = {
31 -1.0f, // Right.
32 -1.0f, // Left.
33 +1.0f, // Up.
34 +1.0f, // Down.
35 -1.0f, // Back.
36 -1.0f, // Front.
37};
38
39IBL* gfx_make_ibl(GfxCore* gfxcore) {
40 assert(gfxcore);
41
42 IBL* ibl = calloc(1, sizeof(IBL));
43 if (!ibl) {
44 return 0;
45 }
46
47 if (!(ibl->quad = gfx_make_quad_11(gfxcore))) {
48 goto cleanup;
49 }
50
51 // We only need the BRDF integration once since we are caching the map, but
52 // compiling the shader up front may lead to fewer surprises. Not that the
53 // shader is fully compiled up front anyway, since the driver will typically
54 // defer full compilation to the first draw call.
55 if (!(ibl->brdf_integration_map_shader =
56 gfx_make_brdf_integration_map_shader(gfxcore))) {
57 goto cleanup;
58 }
59
60 if (!(ibl->irradiance_map_shader = gfx_make_irradiance_map_shader(gfxcore))) {
61 goto cleanup;
62 }
63
64 if (!(ibl->prefiltered_environment_map_shader =
65 gfx_make_prefiltered_environment_map_shader(gfxcore))) {
66 goto cleanup;
67 }
68
69 // Create an empty framebuffer for now. Will attach the colour buffer later
70 // as we render the faces of the cube.
71 if (!(ibl->framebuffer = gfx_make_framebuffer(
72 gfxcore,
73 &(FrameBufferDesc){
74 .colour =
75 (FrameBufferAttachment){.type = FrameBufferNoAttachment},
76 .depth = (FrameBufferAttachment){
77 .type = FrameBufferNoAttachment}}))) {
78 goto cleanup;
79 }
80
81 // TODO: Debug the camera rotations. Irradiance debug output should appear
82 // just like the input cubemap.
83
84 // Right.
85 ibl->rotations[0] = mat4_lookat(
86 /*position=*/vec3_make(0, 0, 0),
87 /*target=*/vec3_make(1, 0, 0),
88 /*up=*/vec3_make(0, 1, 0));
89 // Left.
90 ibl->rotations[1] = mat4_lookat(
91 /*position=*/vec3_make(0, 0, 0),
92 /*target=*/vec3_make(-1, 0, 0),
93 /*up=*/vec3_make(0, 1, 0));
94 // Up.
95 ibl->rotations[2] = mat4_lookat(
96 /*position=*/vec3_make(0, 0, 0),
97 /*target=*/vec3_make(0, 1, 0),
98 /*up=*/vec3_make(0, 0, 1));
99 // Down.
100 ibl->rotations[3] = mat4_lookat(
101 /*position=*/vec3_make(0, 0, 0),
102 /*target=*/vec3_make(0, -1, 0),
103 /*up=*/vec3_make(0, 0, -1));
104 // Back.
105 ibl->rotations[4] = mat4_lookat(
106 /*position=*/vec3_make(0, 0, 0),
107 /*target=*/vec3_make(0, 0, 1),
108 /*up=*/vec3_make(0, 1, 0));
109 // Front.
110 ibl->rotations[5] = mat4_lookat(
111 /*position=*/vec3_make(0, 0, 0),
112 /*target=*/vec3_make(0, 0, -1),
113 /*up=*/vec3_make(0, 1, 0));
114
115 return ibl;
116
117cleanup:
118 gfx_destroy_ibl(gfxcore, &ibl);
119 return 0;
120}
121
122void gfx_destroy_ibl(GfxCore* gfxcore, IBL** ibl) {
123 if (!ibl) {
124 return;
125 }
126 if ((*ibl)->quad) {
127 gfx_destroy_geometry(gfxcore, &(*ibl)->quad);
128 }
129 if ((*ibl)->brdf_integration_map_shader) {
130 gfx_destroy_shader_program(gfxcore, &(*ibl)->brdf_integration_map_shader);
131 }
132 if ((*ibl)->irradiance_map_shader) {
133 gfx_destroy_shader_program(gfxcore, &(*ibl)->irradiance_map_shader);
134 }
135 if ((*ibl)->prefiltered_environment_map_shader) {
136 gfx_destroy_shader_program(
137 gfxcore, &(*ibl)->prefiltered_environment_map_shader);
138 }
139 if ((*ibl)->brdf_integration_map) {
140 gfx_destroy_texture(gfxcore, &(*ibl)->brdf_integration_map);
141 }
142 if ((*ibl)->framebuffer) {
143 gfx_destroy_framebuffer(gfxcore, &(*ibl)->framebuffer);
144 }
145 free(*ibl);
146 *ibl = 0;
147}
148
149Texture* gfx_make_brdf_integration_map(
150 IBL* ibl, GfxCore* gfxcore, int width, int height) {
151 assert(ibl);
152 assert(gfxcore);
153
154 if (ibl->brdf_integration_map) {
155 return ibl->brdf_integration_map;
156 }
157
158 bool success = false;
159
160 if (!(ibl->brdf_integration_map = gfx_make_texture(
161 gfxcore, &(TextureDesc){
162 .width = width,
163 .height = height,
164 .depth = 1,
165 .dimension = Texture2D,
166 .format = TextureRG16F,
167 .filtering = LinearFiltering,
168 .wrap = ClampToEdge,
169 .mipmaps = false}))) {
170 goto cleanup;
171 }
172
173 gfx_activate_framebuffer(ibl->framebuffer);
174 gfx_framebuffer_set_viewport(ibl->framebuffer, 0, 0, width, height);
175 gfx_activate_shader_program(ibl->brdf_integration_map_shader);
176 if (!gfx_framebuffer_attach_colour(
177 ibl->framebuffer, &(FrameBufferAttachment){
178 .type = FrameBufferTexture,
179 .texture.texture = ibl->brdf_integration_map,
180 .texture.mip_level = 0})) {
181 goto cleanup;
182 }
183 gfx_render_geometry(ibl->quad);
184
185 success = true;
186
187cleanup:
188 gfx_deactivate_shader_program(ibl->brdf_integration_map_shader);
189 gfx_deactivate_framebuffer(ibl->framebuffer);
190 if (!success && ibl->brdf_integration_map) {
191 gfx_destroy_texture(gfxcore, &ibl->brdf_integration_map);
192 return 0;
193 } else {
194 return ibl->brdf_integration_map;
195 }
196}
197
198Texture* gfx_make_irradiance_map(
199 IBL* ibl, GfxCore* gfxcore, const Texture* environment_map, int width,
200 int height) {
201 assert(ibl);
202 assert(gfxcore);
203 assert(environment_map);
204
205 bool success = false;
206
207 Texture* irradiance_map = 0;
208
209 // TODO: Could define colour-renderable texture formats separately to make
210 // framebuffer creation less error-prone. Or, at the very least, validate the
211 // choice at runtime.
212 //
213 // Make sure to use a float colour format to avoid [0,1] clamping when the
214 // irradiance values are computed!
215 if (!(irradiance_map = gfx_make_texture(
216 gfxcore, &(TextureDesc){
217 .width = width,
218 .height = height,
219 .depth = 1,
220 .dimension = TextureCubeMap,
221 .format = TextureR11G11B10F,
222 .filtering = LinearFiltering,
223 .mipmaps = false}))) {
224 goto cleanup;
225 }
226
227 gfx_activate_framebuffer(ibl->framebuffer);
228 gfx_framebuffer_set_viewport(ibl->framebuffer, 0, 0, width, height);
229 gfx_activate_shader_program(ibl->irradiance_map_shader);
230 gfx_set_texture_uniform(ibl->irradiance_map_shader, "Sky", environment_map);
231 for (int i = 0; i < 6; ++i) {
232 if (!gfx_framebuffer_attach_colour(
233 ibl->framebuffer, &(FrameBufferAttachment){
234 .type = FrameBufferCubemapTexture,
235 .cubemap.face = faces[i],
236 .cubemap.texture = irradiance_map})) {
237 goto cleanup;
238 }
239 gfx_set_float_uniform(ibl->irradiance_map_shader, "Flip", flips[i]);
240 gfx_set_mat4_uniform(
241 ibl->irradiance_map_shader, "CameraRotation", &ibl->rotations[i]);
242 gfx_apply_uniforms(ibl->irradiance_map_shader);
243 gfx_render_geometry(ibl->quad);
244 }
245
246 success = true;
247
248cleanup:
249 gfx_deactivate_shader_program(ibl->irradiance_map_shader);
250 gfx_deactivate_framebuffer(ibl->framebuffer);
251 if (!success && irradiance_map) {
252 gfx_destroy_texture(gfxcore, &irradiance_map);
253 return 0;
254 } else {
255 return irradiance_map;
256 }
257}
258
259Texture* gfx_make_prefiltered_environment_map(
260 IBL* ibl, GfxCore* gfxcore, const Texture* environment_map, int width,
261 int height, int* max_mip_level) {
262 assert(ibl);
263 assert(gfxcore);
264 assert(environment_map);
265 assert(max_mip_level);
266
267 bool success = false;
268
269 Texture* prefiltered_env_map = 0;
270
271 if (!(prefiltered_env_map = gfx_make_texture(
272 gfxcore, &(TextureDesc){
273 .width = width,
274 .height = height,
275 .depth = 1,
276 .dimension = TextureCubeMap,
277 .format = TextureR11G11B10F,
278 .filtering = LinearFiltering,
279 .mipmaps = true}))) {
280 goto cleanup;
281 }
282
283 gfx_activate_framebuffer(ibl->framebuffer);
284 gfx_activate_shader_program(ibl->prefiltered_environment_map_shader);
285 gfx_set_texture_uniform(
286 ibl->prefiltered_environment_map_shader, "Sky", environment_map);
287 const int max_mip = (int)(rlog2(min(width, height)));
288 for (int mip = 0; mip <= max_mip; ++mip) {
289 const int mip_width = width >> mip;
290 const int mip_height = height >> mip;
291 const float roughness = (float)mip / (float)(max_mip);
292 gfx_framebuffer_set_viewport(ibl->framebuffer, 0, 0, mip_width, mip_height);
293 gfx_set_float_uniform(
294 ibl->prefiltered_environment_map_shader, "Roughness", roughness);
295
296 for (int i = 0; i < 6; ++i) {
297 if (!gfx_framebuffer_attach_colour(
298 ibl->framebuffer, &(FrameBufferAttachment){
299 .type = FrameBufferCubemapTexture,
300 .cubemap.face = faces[i],
301 .cubemap.mip_level = mip,
302 .cubemap.texture = prefiltered_env_map})) {
303 goto cleanup;
304 }
305 gfx_set_float_uniform(
306 ibl->prefiltered_environment_map_shader, "Flip", flips[i]);
307 gfx_set_mat4_uniform(
308 ibl->prefiltered_environment_map_shader, "CameraRotation",
309 &ibl->rotations[i]);
310 gfx_apply_uniforms(ibl->prefiltered_environment_map_shader);
311 gfx_render_geometry(ibl->quad);
312 }
313 }
314
315 *max_mip_level = max_mip;
316
317 success = true;
318
319cleanup:
320 gfx_deactivate_shader_program(ibl->prefiltered_environment_map_shader);
321 gfx_deactivate_framebuffer(ibl->framebuffer);
322 if (!success && prefiltered_env_map) {
323 gfx_destroy_texture(gfxcore, &prefiltered_env_map);
324 return 0;
325 } else {
326 return prefiltered_env_map;
327 }
328}
diff --git a/src/util/shader.c b/src/util/shader.c
new file mode 100644
index 0000000..f5c22cc
--- /dev/null
+++ b/src/util/shader.c
@@ -0,0 +1,136 @@
1#include <gfx/util/shader.h>
2
3#include <gfx/core.h>
4#include <shaders/brdf_integration_map.frag.h>
5#include <shaders/cook_torrance.frag.h>
6#include <shaders/cook_torrance.vert.h>
7#include <shaders/cubemap_filtering.vert.h>
8#include <shaders/debug3d.frag.h>
9#include <shaders/debug3d.vert.h>
10#include <shaders/immediate_mode.frag.h>
11#include <shaders/immediate_mode.vert.h>
12#include <shaders/irradiance_map.frag.h>
13#include <shaders/prefiltered_environment_map.frag.h>
14#include <shaders/quad.vert.h>
15#include <shaders/skyquad.frag.h>
16#include <shaders/skyquad.vert.h>
17#include <shaders/view_normal_mapped_normals.frag.h>
18#include <shaders/view_normal_mapped_normals.vert.h>
19#include <shaders/view_normals.frag.h>
20#include <shaders/view_normals.vert.h>
21#include <shaders/view_tangents.frag.h>
22#include <shaders/view_tangents.vert.h>
23#include <shaders/view_texture.frag.h>
24#include <shaders/view_texture.vert.h>
25
26#include <assert.h>
27#include <string.h>
28
29static ShaderProgram* make_shader_program(
30 GfxCore* gfxcore, const char* vert_source, const char* frag_source,
31 const ShaderCompilerDefine* defines, size_t num_defines) {
32 assert(gfxcore);
33 assert(vert_source);
34 assert(frag_source);
35
36 Shader* vert = 0;
37 Shader* frag = 0;
38
39 ShaderDesc vertex_shader_desc = {
40 .code = vert_source, .type = VertexShader, .num_defines = num_defines};
41 ShaderDesc fragment_shader_desc = {
42 .code = frag_source, .type = FragmentShader, .num_defines = num_defines};
43 if (num_defines > 0) {
44 memcpy(
45 vertex_shader_desc.defines, defines,
46 num_defines * sizeof(ShaderCompilerDefine));
47 memcpy(
48 fragment_shader_desc.defines, defines,
49 num_defines * sizeof(ShaderCompilerDefine));
50 }
51 vert = gfx_make_shader(gfxcore, &vertex_shader_desc);
52 if (!vert) {
53 goto cleanup;
54 }
55 frag = gfx_make_shader(gfxcore, &fragment_shader_desc);
56 if (!frag) {
57 goto cleanup;
58 }
59
60 ShaderProgramDesc shader_program_desc = {
61 .vertex_shader = vert, .fragment_shader = frag};
62 ShaderProgram* prog = gfx_make_shader_program(gfxcore, &shader_program_desc);
63 if (!prog) {
64 goto cleanup;
65 }
66 return prog;
67
68cleanup:
69 if (vert) {
70 gfx_destroy_shader(gfxcore, &vert);
71 }
72 if (frag) {
73 gfx_destroy_shader(gfxcore, &frag);
74 }
75 return 0;
76}
77
78ShaderProgram* gfx_make_brdf_integration_map_shader(GfxCore* gfxcore) {
79 return make_shader_program(
80 gfxcore, quad_vert, brdf_integration_map_frag, 0, 0);
81}
82
83ShaderProgram* gfx_make_cook_torrance_shader(GfxCore* gfxcore) {
84 return make_shader_program(
85 gfxcore, cook_torrance_vert, cook_torrance_frag, 0, 0);
86}
87
88ShaderProgram* gfx_make_cook_torrance_shader_perm(
89 GfxCore* gfxcore, const ShaderCompilerDefine* defines, size_t num_defines) {
90 return make_shader_program(
91 gfxcore, cook_torrance_vert, cook_torrance_frag, defines, num_defines);
92}
93
94ShaderProgram* gfx_make_immediate_mode_shader(GfxCore* gfxcore) {
95 return make_shader_program(
96 gfxcore, immediate_mode_vert, immediate_mode_frag, 0, 0);
97}
98
99ShaderProgram* gfx_make_irradiance_map_shader(GfxCore* gfxcore) {
100 return make_shader_program(
101 gfxcore, cubemap_filtering_vert, irradiance_map_frag, 0, 0);
102}
103
104ShaderProgram* gfx_make_prefiltered_environment_map_shader(GfxCore* gfxcore) {
105 return make_shader_program(
106 gfxcore, cubemap_filtering_vert, prefiltered_environment_map_frag, 0, 0);
107}
108
109ShaderProgram* gfx_make_debug3d_shader(GfxCore* gfxcore) {
110 return make_shader_program(gfxcore, debug3d_vert, debug3d_frag, 0, 0);
111}
112
113ShaderProgram* gfx_make_skyquad_shader(GfxCore* gfxcore) {
114 return make_shader_program(gfxcore, skyquad_vert, skyquad_frag, 0, 0);
115}
116
117ShaderProgram* gfx_make_view_normal_mapped_normals_shader(GfxCore* gfxcore) {
118 return make_shader_program(
119 gfxcore, view_normal_mapped_normals_vert, view_normal_mapped_normals_frag,
120 0, 0);
121}
122
123ShaderProgram* gfx_make_view_normals_shader(GfxCore* gfxcore) {
124 return make_shader_program(
125 gfxcore, view_normals_vert, view_normals_frag, 0, 0);
126}
127
128ShaderProgram* gfx_make_view_tangents_shader(GfxCore* gfxcore) {
129 return make_shader_program(
130 gfxcore, view_tangents_vert, view_tangents_frag, 0, 0);
131}
132
133ShaderProgram* gfx_make_view_texture_shader(GfxCore* gfxcore) {
134 return make_shader_program(
135 gfxcore, view_texture_vert, view_texture_frag, 0, 0);
136}
diff --git a/src/util/skyquad.c b/src/util/skyquad.c
new file mode 100644
index 0000000..08fa044
--- /dev/null
+++ b/src/util/skyquad.c
@@ -0,0 +1,161 @@
1#include <gfx/util/skyquad.h>
2
3#include <gfx/core.h>
4#include <gfx/gfx.h>
5#include <gfx/scene/light.h>
6#include <gfx/scene/material.h>
7#include <gfx/scene/mesh.h>
8#include <gfx/scene/node.h>
9#include <gfx/scene/object.h>
10#include <gfx/scene/scene.h>
11#include <gfx/util/geometry.h>
12#include <gfx/util/shader.h>
13
14#include <math/vec4.h>
15
16#include <assert.h>
17
18SceneObject* gfx_make_skyquad(GfxCore* gfxcore, const Texture* texture) {
19 assert(gfxcore);
20 assert(texture);
21
22 ShaderProgram* shader = 0;
23 Geometry* geometry = 0;
24 Material* material = 0;
25 Mesh* mesh = 0;
26 SceneObject* object = 0;
27
28 shader = gfx_make_skyquad_shader(gfxcore);
29 if (!shader) {
30 goto cleanup;
31 }
32
33 geometry = gfx_make_quad_11(gfxcore);
34 if (!geometry) {
35 goto cleanup;
36 }
37
38 MaterialDesc material_desc = (MaterialDesc){0};
39 material_desc.uniforms[0] = (ShaderUniform){
40 .type = UniformTexture,
41 .value.texture = texture,
42 .name = sstring_make("Skyquad")};
43 material_desc.num_uniforms = 1;
44 material = gfx_make_material(&material_desc);
45 if (!material) {
46 goto cleanup;
47 }
48
49 MeshDesc mesh_desc = (MeshDesc){0};
50 mesh_desc.geometry = geometry;
51 mesh_desc.material = material;
52 mesh_desc.shader = shader;
53 mesh = gfx_make_mesh(&mesh_desc);
54 if (!mesh) {
55 goto cleanup;
56 }
57
58 object = gfx_make_object(&(ObjectDesc){.num_meshes = 1, .meshes = {mesh}});
59 if (!object) {
60 goto cleanup;
61 }
62
63 return object;
64
65cleanup:
66 if (shader) {
67 gfx_destroy_shader_program(gfxcore, &shader);
68 }
69 if (geometry) {
70 gfx_destroy_geometry(gfxcore, &geometry);
71 }
72 if (material) {
73 gfx_destroy_material(&material);
74 }
75 if (mesh) {
76 gfx_destroy_mesh(&mesh);
77 }
78 if (object) {
79 gfx_destroy_object(&object);
80 }
81 return false;
82}
83
84/// Create an environment light node.
85static SceneNode* make_environment_light(
86 SceneNode* root, const Texture* environment_map) {
87 assert(root);
88 assert(environment_map);
89
90 Light* light = 0;
91 SceneNode* light_node = 0;
92
93 light = gfx_make_light(&(LightDesc){
94 .type = EnvironmentLightType,
95 .light = {(EnvironmentLightDesc){.environment_map = environment_map}}});
96 if (!light) {
97 goto cleanup;
98 }
99
100 light_node = gfx_make_light_node(light);
101 if (!light_node) {
102 goto cleanup;
103 }
104 gfx_set_node_parent(light_node, root);
105
106 return light_node;
107
108cleanup:
109 if (light) {
110 gfx_destroy_light(&light);
111 }
112 if (light_node) {
113 gfx_destroy_node(&light_node);
114 }
115 return 0;
116}
117
118SceneNode* gfx_setup_skyquad(
119 GfxCore* gfxcore, SceneNode* root, const Texture* environment_map) {
120 assert(gfxcore);
121 assert(root);
122 assert(environment_map);
123
124 SceneObject* skyquad_object = 0;
125 SceneNode* object_node = 0;
126 SceneNode* light_node = 0;
127
128 // Create the skyquad object.
129 skyquad_object = gfx_make_skyquad(gfxcore, environment_map);
130 if (!skyquad_object) {
131 goto cleanup;
132 }
133
134 // Create an object node to render the skyquad in the background.
135 object_node = gfx_make_object_node(skyquad_object);
136 if (!object_node) {
137 goto cleanup;
138 }
139 gfx_set_node_parent(object_node, root);
140
141 // Create an environment light node under which to root objects affected by
142 // the skyquad.
143 light_node = make_environment_light(root, environment_map);
144 if (!light_node) {
145 goto cleanup;
146 }
147
148 return light_node;
149
150cleanup:
151 if (skyquad_object) {
152 gfx_destroy_object(&skyquad_object);
153 }
154 if (object_node) {
155 gfx_destroy_node(&object_node);
156 }
157 if (light_node) {
158 gfx_destroy_node(&light_node);
159 }
160 return 0;
161}