aboutsummaryrefslogtreecommitdiff
path: root/src/llr
diff options
context:
space:
mode:
Diffstat (limited to 'src/llr')
-rw-r--r--src/llr/light.c42
-rw-r--r--src/llr/light_impl.h25
-rw-r--r--src/llr/llr.c406
-rw-r--r--src/llr/llr_impl.h86
-rw-r--r--src/llr/material.c57
-rw-r--r--src/llr/material_impl.h15
-rw-r--r--src/llr/mesh.c24
-rw-r--r--src/llr/mesh_impl.h9
8 files changed, 664 insertions, 0 deletions
diff --git a/src/llr/light.c b/src/llr/light.c
new file mode 100644
index 0000000..0fa1522
--- /dev/null
+++ b/src/llr/light.c
@@ -0,0 +1,42 @@
1#include "light_impl.h"
2
3#include "memory.h"
4#include "scene/node_impl.h"
5
6#include <error.h>
7
8static void make_environment_light(
9 Light* light, const EnvironmentLightDesc* desc) {
10 assert(light);
11 assert(desc);
12 light->type = EnvironmentLightType;
13 light->environment.environment_map = desc->environment_map;
14}
15
16Light* gfx_make_light(const LightDesc* desc) {
17 assert(desc);
18
19 Light* light = mem_alloc_light();
20
21 switch (desc->type) {
22 case EnvironmentLightType:
23 make_environment_light(light, &desc->light.environment);
24 break;
25 default:
26 log_error("Unhandled light type");
27 gfx_destroy_light(&light);
28 return 0;
29 }
30
31 return light;
32}
33
34void gfx_destroy_light(Light** light) {
35 assert(light);
36 if (*light) {
37 if ((*light)->parent.val) {
38 gfx_del_node((*light)->parent);
39 }
40 mem_free_light(light);
41 }
42}
diff --git a/src/llr/light_impl.h b/src/llr/light_impl.h
new file mode 100644
index 0000000..5ec8145
--- /dev/null
+++ b/src/llr/light_impl.h
@@ -0,0 +1,25 @@
1#pragma once
2
3#include <gfx/llr/light.h>
4
5#include "scene/types.h"
6
7typedef struct Texture Texture;
8
9/// An environment light.
10typedef struct EnvironmentLight {
11 const Texture* environment_map;
12 const Texture* irradiance_map; // Renderer implementation.
13 const Texture* prefiltered_environment_map; // Renderer implementation.
14 int max_reflection_lod; // Mandatory when prefiltered_environment_map is
15 // given.
16} EnvironmentLight;
17
18/// A scene light.
19typedef struct Light {
20 LightType type;
21 union {
22 EnvironmentLight environment;
23 };
24 node_idx parent; // Parent SceneNode.
25} Light;
diff --git a/src/llr/llr.c b/src/llr/llr.c
new file mode 100644
index 0000000..fe02c0d
--- /dev/null
+++ b/src/llr/llr.c
@@ -0,0 +1,406 @@
1#include "light_impl.h"
2#include "llr_impl.h"
3#include "mesh_impl.h"
4
5#include "llr/material_impl.h"
6#include "scene/animation_impl.h"
7
8#include <gfx/core.h>
9#include <gfx/util/ibl.h>
10
11#include <cassert.h>
12
13static const int IRRADIANCE_MAP_WIDTH = 1024;
14static const int IRRADIANCE_MAP_HEIGHT = 1024;
15static const int PREFILTERED_ENVIRONMENT_MAP_WIDTH = 128;
16static const int PREFILTERED_ENVIRONMENT_MAP_HEIGHT = 128;
17static const int BRDF_INTEGRATION_MAP_WIDTH = 512;
18static const int BRDF_INTEGRATION_MAP_HEIGHT = 512;
19
20/// Initialize renderer state for IBL.
21static bool init_ibl(LLR* renderer) {
22 assert(renderer);
23 assert(!renderer->ibl);
24 assert(!renderer->brdf_integration_map);
25
26 if (!((renderer->ibl = gfx_make_ibl(renderer->gfxcore)))) {
27 return false;
28 }
29
30 if (!((renderer->brdf_integration_map = gfx_make_brdf_integration_map(
31 renderer->ibl, renderer->gfxcore, BRDF_INTEGRATION_MAP_WIDTH,
32 BRDF_INTEGRATION_MAP_HEIGHT)))) {
33 return false;
34 }
35
36 return true;
37}
38
39/// Compute irradiance and prefiltered environment maps for the light if they
40/// have not been already computed.
41///
42/// This is done lazily here, and not when the light is created, because we
43/// need an IBL instance to do this and it is more convenient for the public
44/// API to create lights without worrying about those details. It also makes the
45/// public API cheaper, since the maps are only computed when they are actually
46/// needed.
47static bool set_up_environment_light(LLR* renderer, EnvironmentLight* light) {
48 assert(renderer);
49 assert(light);
50 assert(renderer->ibl);
51 assert(renderer->brdf_integration_map);
52
53 if (light->irradiance_map) {
54 assert(light->prefiltered_environment_map);
55 return true;
56 }
57
58 // For convenience.
59 GfxCore* gfxcore = renderer->gfxcore;
60
61 Texture* irradiance_map = 0;
62 Texture* prefiltered_environment_map = 0;
63
64 if (!((irradiance_map = gfx_make_irradiance_map(
65 renderer->ibl, gfxcore, light->environment_map,
66 IRRADIANCE_MAP_WIDTH, IRRADIANCE_MAP_HEIGHT)))) {
67 goto cleanup;
68 }
69
70 int max_mip_level = 0;
71 if (!((prefiltered_environment_map = gfx_make_prefiltered_environment_map(
72 renderer->ibl, gfxcore, light->environment_map,
73 PREFILTERED_ENVIRONMENT_MAP_WIDTH,
74 PREFILTERED_ENVIRONMENT_MAP_HEIGHT, &max_mip_level)))) {
75 goto cleanup;
76 }
77
78 light->irradiance_map = irradiance_map;
79 light->prefiltered_environment_map = prefiltered_environment_map;
80 light->max_reflection_lod = max_mip_level;
81
82 return true;
83
84cleanup:
85 if (irradiance_map) {
86 gfx_destroy_texture(gfxcore, &irradiance_map);
87 }
88 if (prefiltered_environment_map) {
89 gfx_destroy_texture(gfxcore, &prefiltered_environment_map);
90 }
91 return false;
92}
93
94static void configure_light(LLR* renderer, Light* light) {
95 assert(renderer);
96 assert(light);
97
98 // For convenience.
99 ShaderProgram* const shader = renderer->shader;
100
101 switch (light->type) {
102 case EnvironmentLightType: {
103 EnvironmentLight* env = &light->environment;
104
105 const bool initialized = set_up_environment_light(renderer, env);
106 ASSERT(initialized);
107 assert(env->environment_map);
108 assert(env->irradiance_map);
109 assert(env->prefiltered_environment_map);
110 assert(renderer->brdf_integration_map);
111
112 gfx_set_texture_uniform(
113 shader, "BRDFIntegrationMap", renderer->brdf_integration_map);
114 gfx_set_texture_uniform(shader, "Sky", env->environment_map);
115 gfx_set_texture_uniform(shader, "IrradianceMap", env->irradiance_map);
116 gfx_set_texture_uniform(
117 shader, "PrefilteredEnvironmentMap", env->prefiltered_environment_map);
118 gfx_set_float_uniform(
119 shader, "MaxReflectionLOD", (float)env->max_reflection_lod);
120
121 break;
122 }
123 default:
124 assert(false); // TODO: Implement other light types.
125 break;
126 }
127}
128
129static void configure_state(LLR* renderer) {
130 assert(renderer);
131
132 // Check if anything changed first so that we don't call gfx_apply_uniforms()
133 // unnecessarily.
134 const bool nothing_changed = (renderer->changed_flags == 0);
135 if (nothing_changed) {
136 return;
137 }
138 // Setting a null shader is also allowed, in which case there is nothing to
139 // configure.
140 if (renderer->shader == 0) {
141 renderer->shader_changed = false;
142 return;
143 }
144
145 // For convenience.
146 ShaderProgram* const shader = renderer->shader;
147 const mat4* const model = &renderer->matrix_stack[renderer->stack_pointer];
148
149 // TODO: Check to see which ones the shader actually uses and avoid
150 // computing the unnecessary matrices.
151
152 if (renderer->matrix_changed || renderer->shader_changed) {
153 renderer->matrix_changed = false;
154
155 gfx_set_mat4_uniform(shader, "Model", model);
156 gfx_set_mat4_uniform(shader, "ModelMatrix", model);
157 }
158
159 // TODO: camera_changed is not set anywhere. Need to think how imm primitive
160 // rendering and imm mesh rendering work together. We could treat imm
161 // primitive calls like setting a new shader.
162 if (renderer->camera_changed || renderer->shader_changed) {
163 renderer->camera_changed = false;
164
165 // Set all supported camera-related uniforms. Shaders can choose which ones
166 // to use.
167 const mat4 modelview = mat4_mul(renderer->view, *model);
168 const mat4 view_proj = mat4_mul(renderer->projection, renderer->view);
169 const mat4 mvp = mat4_mul(renderer->projection, modelview);
170
171 gfx_set_mat4_uniform(shader, "Modelview", &modelview);
172 gfx_set_mat4_uniform(shader, "View", &renderer->view);
173 gfx_set_mat4_uniform(shader, "Projection", &renderer->projection);
174 gfx_set_mat4_uniform(shader, "ViewProjection", &view_proj);
175 gfx_set_mat4_uniform(shader, "MVP", &mvp);
176 gfx_set_vec3_uniform(shader, "CameraPosition", renderer->camera_position);
177 gfx_set_mat4_uniform(shader, "CameraRotation", &renderer->camera_rotation);
178 gfx_set_float_uniform(shader, "Fovy", renderer->fovy);
179 gfx_set_float_uniform(shader, "Aspect", renderer->aspect);
180 }
181
182 if (renderer->lights_changed || renderer->shader_changed) {
183 renderer->lights_changed = false;
184
185 // TODO: Could do better by only setting the lights that have actually
186 // changed.
187 // TODO: Will also need to pass the number of lights to the shader once the
188 // other light types are implemented.
189 for (int i = 0; i < renderer->num_lights; ++i) {
190 configure_light(renderer, renderer->lights[i]);
191 }
192 }
193
194 if (renderer->skeleton_changed || renderer->shader_changed) {
195 renderer->skeleton_changed = false;
196
197 if (renderer->num_joints > 0) {
198 gfx_set_mat4_array_uniform(
199 shader, "JointMatrices", renderer->joint_matrices,
200 renderer->num_joints);
201 }
202 }
203
204 if (renderer->material_changed || renderer->shader_changed) {
205 renderer->material_changed = false;
206
207 gfx_material_activate(renderer->shader, renderer->material);
208 }
209
210 if (renderer->shader_changed) {
211 renderer->shader_changed = false;
212 gfx_activate_shader_program(renderer->shader);
213 }
214
215 // TODO: At present, this results in many redundant calls to
216 // glGetUniformLocation() and glUniformXyz(). Look at the trace.
217 //
218 // TODO: Could add to qapitrace functionality to detect redundant calls and
219 // other inefficiencies. Maybe ask in the Github first if there would be
220 // interest in this.
221 //
222 // Must be called after activating the program.
223 gfx_apply_uniforms(renderer->shader);
224}
225
226bool gfx_llr_make(LLR* renderer, GfxCore* gfxcore) {
227 assert(renderer);
228 assert(gfxcore);
229
230 renderer->gfxcore = gfxcore;
231 if (!init_ibl(renderer)) {
232 goto cleanup;
233 }
234 gfx_llr_load_identity(renderer);
235 renderer->view = mat4_id();
236 renderer->projection = mat4_id();
237 renderer->camera_rotation = mat4_id();
238 return true;
239
240cleanup:
241 gfx_llr_destroy(renderer);
242 return false;
243}
244
245void gfx_llr_destroy(LLR* renderer) {
246 assert(renderer);
247 assert(renderer->gfxcore);
248
249 if (renderer->brdf_integration_map) {
250 gfx_destroy_texture(renderer->gfxcore, &renderer->brdf_integration_map);
251 }
252
253 // TODO: Do this once the IBL from the scene renderer is gone.
254 if (renderer->ibl) {
255 // gfx_destroy_ibl(renderer->gfxcore, &renderer->ibl);
256 }
257}
258
259void gfx_llr_set_shader(LLR* renderer, ShaderProgram* shader) {
260 assert(renderer);
261 // null shader is allowed, so do not assert it.
262
263 // It's important to not set shader_changed unnecessarily, since that would
264 // re-trigger the setting of uniforms.
265 if (renderer->shader != shader) {
266 renderer->shader = shader;
267 renderer->shader_changed = true;
268 }
269}
270
271void gfx_llr_push_light(LLR* renderer, Light* light) {
272 assert(renderer);
273 assert(light);
274 assert(renderer->num_lights >= 0);
275 ASSERT(renderer->num_lights < GFX_LLR_MAX_NUM_LIGHTS);
276
277 renderer->lights[renderer->num_lights++] = light;
278 renderer->lights_changed = true;
279}
280
281void gfx_llr_pop_light(LLR* renderer) {
282 assert(renderer);
283 ASSERT(renderer->num_lights > 0);
284
285 renderer->lights[--renderer->num_lights] = 0;
286 renderer->lights_changed = true;
287}
288
289void gfx_llr_set_skeleton(
290 LLR* renderer, const Anima* anima, const Skeleton* skeleton) {
291 assert(renderer);
292 assert(anima);
293 assert(skeleton);
294 assert(skeleton->num_joints <= GFX_MAX_NUM_JOINTS);
295
296 for (size_t i = 0; i < skeleton->num_joints; ++i) {
297 const joint_idx joint_index = skeleton->joints[i];
298 const Joint* joint = &anima->joints[joint_index];
299 renderer->joint_matrices[i] = joint->joint_matrix;
300 }
301 renderer->num_joints = skeleton->num_joints;
302 renderer->skeleton_changed = true;
303}
304
305void gfx_llr_clear_skeleton(LLR* renderer) {
306 assert(renderer);
307
308 renderer->num_joints = 0;
309 renderer->skeleton_changed = true;
310}
311
312void gfx_llr_set_material(LLR* renderer, const Material* material) {
313 assert(renderer);
314 assert(material);
315
316 renderer->material = material;
317 renderer->material_changed = true;
318}
319
320void gfx_llr_set_camera(LLR* renderer, const Camera* camera) {
321 assert(renderer);
322
323 renderer->camera_position = camera->spatial.p;
324 renderer->camera_rotation =
325 mat4_rotation(spatial3_transform(&camera->spatial));
326 renderer->view = spatial3_inverse_transform(&camera->spatial);
327 renderer->projection = camera->projection;
328 // Assuming a perspective matrix.
329 renderer->fovy = (R)atan(1.0 / (mat4_at(camera->projection, 1, 1))) * 2;
330 renderer->camera_changed = true;
331}
332
333void gfx_llr_set_aspect(LLR* renderer, float aspect) {
334 assert(renderer);
335
336 renderer->aspect = aspect;
337 renderer->camera_changed = true;
338}
339
340void gfx_llr_render_geometry(LLR* renderer, const Geometry* geometry) {
341 assert(renderer);
342 assert(geometry);
343
344 configure_state(renderer);
345 gfx_render_geometry(geometry);
346}
347
348void gfx_llr_render_mesh(LLR* renderer, const Mesh* mesh) {
349 assert(renderer);
350 assert(mesh);
351 assert(mesh->geometry);
352 assert(mesh->material);
353 assert(mesh->shader);
354
355 gfx_llr_set_material(renderer, mesh->material);
356 gfx_llr_set_shader(renderer, mesh->shader);
357 gfx_llr_render_geometry(renderer, mesh->geometry);
358}
359
360void gfx_llr_load_identity(LLR* renderer) {
361 assert(renderer);
362
363 renderer->matrix_stack[0] = mat4_id();
364 renderer->stack_pointer = 0;
365 renderer->matrix_changed = true;
366}
367
368void gfx_llr_push_matrix(LLR* renderer, const mat4* matrix) {
369 assert(renderer);
370 assert(matrix);
371 assert(renderer->stack_pointer >= 0);
372 ASSERT(renderer->stack_pointer < GFX_LLR_MAX_NUM_MATRICES);
373
374 renderer->stack_pointer += 1;
375 renderer->matrix_stack[renderer->stack_pointer] =
376 mat4_mul(*matrix, renderer->matrix_stack[renderer->stack_pointer - 1]);
377 renderer->matrix_changed = true;
378}
379
380void gfx_llr_pop_matrix(LLR* renderer) {
381 assert(renderer);
382 ASSERT(renderer->stack_pointer > 0);
383
384 // For debugging, zero out the matrix stack as matrices are popped out.
385 memset(
386 &renderer->matrix_stack[renderer->stack_pointer], 0,
387 sizeof(renderer->matrix_stack[0]));
388 renderer->stack_pointer -= 1;
389 renderer->matrix_changed = true;
390}
391
392void gfx_llr_translate(LLR* renderer, vec3 offset) {
393 assert(renderer);
394
395 const mat4 mat = mat4_translate(offset);
396 gfx_llr_push_matrix(renderer, &mat);
397}
398
399void gfx_llr_set_model_matrix(LLR* renderer, const mat4* model) {
400 assert(renderer);
401 assert(model);
402
403 renderer->matrix_stack[0] = *model;
404 renderer->stack_pointer = 0;
405 renderer->matrix_changed = true;
406}
diff --git a/src/llr/llr_impl.h b/src/llr/llr_impl.h
new file mode 100644
index 0000000..3f6a68f
--- /dev/null
+++ b/src/llr/llr_impl.h
@@ -0,0 +1,86 @@
1#pragma once
2
3#include <gfx/llr/llr.h>
4#include <gfx/sizes.h>
5
6#include <math/mat4.h>
7#include <math/vec3.h>
8
9#include <stdbool.h>
10#include <stddef.h>
11#include <stdint.h>
12
13typedef struct Geometry Geometry;
14typedef struct GfxCore GfxCore;
15typedef struct IBL IBL;
16typedef struct Material Material;
17typedef struct ShaderProgram ShaderProgram;
18typedef struct Texture Texture;
19
20/// Immediate mode renderer.
21///
22/// The renderer caches state changes in memory and only programs the underlying
23/// shader program when a draw call is issued and if anything has changed. This
24/// keeps the number of graphics API calls to a minimum, but requires tracking
25/// state changes. The 'changed' booleans below fulfill this purpose, and
26/// indicate whether a given state has changed since the last draw call.
27///
28/// The renderer must combine state changes accordingly. For example, if only
29/// the lights have changed, then it is sufficient to update light uniforms in
30/// the current shader program. On the other hand, if the shader program has
31/// changed, then the renderer must reconfigure it from scratch and set light
32/// uniforms, camera uniforms, etc.
33///
34/// Note that the shader program API has its own level of caching as well, so
35/// reconfiguration at the level of the renderer does not result in the
36/// worst-case set of graphics API calls.
37typedef struct LLR {
38 GfxCore* gfxcore;
39
40 union {
41 struct {
42 bool shader_changed : 1; // Whether the shader has changed.
43 bool camera_changed : 1; // Whether the camera parameters have changed.
44 bool lights_changed : 1; // Whether the lights have changed.
45 bool skeleton_changed : 1; // Whether the skeleton has changed.
46 bool material_changed : 1; // Whether the material has changed.
47 bool matrix_changed : 1; // Whether the matrix stack has changed.
48 };
49 uint8_t changed_flags;
50 };
51
52 IBL* ibl;
53 Texture* brdf_integration_map;
54
55 ShaderProgram* shader; // Active shader. Not owned.
56
57 const Material* material; // Active material. Not owned.
58
59 vec3 camera_position;
60 mat4 camera_rotation;
61 mat4 view; // Camera view matrix.
62 mat4 projection; // Camera projection matrix.
63 R fovy; // Camera vertical field of view.
64 R aspect; // Aspect ratio.
65
66 // Lights are not const because environment lights store lazily-computed
67 // irradiance maps.
68 Light* lights[GFX_LLR_MAX_NUM_LIGHTS]; // Lights stack.
69 int num_lights; // Number of lights enabled at a given point in time. It
70 // points to one past the top of the stack.
71
72 size_t num_joints;
73 mat4 joint_matrices[GFX_MAX_NUM_JOINTS];
74
75 // The matrix stack contains pre-multiplied matrices.
76 // It is also never empty. The top of the stack is an identity matrix when the
77 // stack is "empty" from the user's perspective.
78 mat4 matrix_stack[GFX_LLR_MAX_NUM_MATRICES];
79 int stack_pointer; // Points to the top of the stack.
80} LLR;
81
82/// Create a new immediate mode renderer.
83bool gfx_llr_make(LLR*, GfxCore*);
84
85/// Destroy the immediate mode renderer.
86void gfx_llr_destroy(LLR*);
diff --git a/src/llr/material.c b/src/llr/material.c
new file mode 100644
index 0000000..f09dd3f
--- /dev/null
+++ b/src/llr/material.c
@@ -0,0 +1,57 @@
1#include "material_impl.h"
2
3#include "memory.h"
4
5#include <gfx/core.h>
6
7static void material_make(Material* material, const MaterialDesc* desc) {
8 assert(material);
9 assert(desc);
10 assert(desc->num_uniforms < GFX_MAX_UNIFORMS_PER_MATERIAL);
11 material->num_uniforms = desc->num_uniforms;
12 for (int i = 0; i < desc->num_uniforms; ++i) {
13 material->uniforms[i] = desc->uniforms[i];
14 }
15}
16
17Material* gfx_make_material(const MaterialDesc* desc) {
18 assert(desc);
19 Material* material = mem_alloc_material();
20 material_make(material, desc);
21 return material;
22}
23
24void gfx_destroy_material(Material** material) { mem_free_material(material); }
25
26static void set_uniform(ShaderProgram* prog, const ShaderUniform* uniform) {
27 switch (uniform->type) {
28 case UniformTexture:
29 gfx_set_texture_uniform(prog, uniform->name.str, uniform->value.texture);
30 break;
31 case UniformMat4:
32 gfx_set_mat4_uniform(prog, uniform->name.str, &uniform->value.mat4);
33 break;
34 case UniformVec3:
35 gfx_set_vec3_uniform(prog, uniform->name.str, uniform->value.vec3);
36 break;
37 case UniformVec4:
38 gfx_set_vec4_uniform(prog, uniform->name.str, uniform->value.vec4);
39 break;
40 case UniformFloat:
41 gfx_set_float_uniform(prog, uniform->name.str, uniform->value.scalar);
42 break;
43 case UniformMat4Array:
44 gfx_set_mat4_array_uniform(
45 prog, uniform->name.str, uniform->value.array.values,
46 uniform->value.array.count);
47 break;
48 }
49}
50
51void gfx_material_activate(ShaderProgram* shader, const Material* material) {
52 assert(material);
53 for (int i = 0; i < material->num_uniforms; ++i) {
54 const ShaderUniform* uniform = &material->uniforms[i];
55 set_uniform(shader, uniform);
56 }
57}
diff --git a/src/llr/material_impl.h b/src/llr/material_impl.h
new file mode 100644
index 0000000..2b7cd89
--- /dev/null
+++ b/src/llr/material_impl.h
@@ -0,0 +1,15 @@
1#pragma once
2
3#include <gfx/llr/material.h>
4
5typedef struct ShaderProgram ShaderProgram;
6
7typedef struct Material {
8 ShaderUniform uniforms[GFX_MAX_UNIFORMS_PER_MATERIAL];
9 int num_uniforms;
10} Material;
11
12/// Activate the material.
13///
14/// This configures the shader uniforms that are specific to the material.
15void gfx_material_activate(ShaderProgram* shader, const Material* material);
diff --git a/src/llr/mesh.c b/src/llr/mesh.c
new file mode 100644
index 0000000..5f9e5d0
--- /dev/null
+++ b/src/llr/mesh.c
@@ -0,0 +1,24 @@
1#include "mesh_impl.h"
2
3#include "memory.h"
4
5#include <assert.h>
6
7static void mesh_make(Mesh* mesh, const MeshDesc* desc) {
8 assert(mesh);
9 assert(desc);
10 assert(desc->geometry);
11 assert(desc->material);
12 assert(desc->shader);
13 mesh->geometry = desc->geometry;
14 mesh->material = desc->material;
15 mesh->shader = desc->shader;
16}
17
18Mesh* gfx_make_mesh(const MeshDesc* desc) {
19 Mesh* mesh = mem_alloc_mesh();
20 mesh_make(mesh, desc);
21 return mesh;
22}
23
24void gfx_destroy_mesh(Mesh** mesh) { mem_free_mesh(mesh); }
diff --git a/src/llr/mesh_impl.h b/src/llr/mesh_impl.h
new file mode 100644
index 0000000..96e60df
--- /dev/null
+++ b/src/llr/mesh_impl.h
@@ -0,0 +1,9 @@
1#pragma once
2
3#include <gfx/llr/mesh.h>
4
5typedef struct Mesh {
6 const Geometry* geometry;
7 const Material* material;
8 ShaderProgram* shader;
9} Mesh;