-
Notifications
You must be signed in to change notification settings - Fork 29
Expand file tree
/
Copy pathclustered_shading.c
More file actions
3753 lines (3337 loc) · 133 KB
/
clustered_shading.c
File metadata and controls
3753 lines (3337 loc) · 133 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include "webgpu/imgui_overlay.h"
#include "webgpu/wgpu_common.h"
/* WebGPU uses depth range [0,1] (zero-to-one), not OpenGL's [-1,1] */
#define CGLM_FORCE_DEPTH_ZERO_TO_ONE
#include <cglm/cglm.h>
#include <cgltf.h>
#define SOKOL_TIME_IMPL
#include <sokol_time.h>
/* Async file loading */
#define SOKOL_FETCH_IMPL
#include <sokol_fetch.h>
#include "core/image_loader.h"
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"
#define CIMGUI_DEFINE_ENUMS_AND_STRUCTS
#endif
#include <cimgui.h>
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
#include <string.h>
/* -------------------------------------------------------------------------- *
* WebGPU Example - Clustered Forward Shading
*
* A simple clustered forward shading renderer with WebGPU. Uses a compute
* shader to build a cluster grid and assign lights to clusters. Renders the
* scene with PBR (Physically Based Rendering) materials loaded from a glTF
* model (Sponza). Supports debug visualization modes for depth, depth slices,
* cluster distances, and lights per cluster.
*
* Ref:
* https://github.com/toji/webgpu-clustered-shading
* http://www.aortiz.me/2018/12/21/CG.html
* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- *
* Constants
* -------------------------------------------------------------------------- */
#define SAMPLE_COUNT (4)
#define DEPTH_FORMAT WGPUTextureFormat_Depth24Plus
/* Clustered shading grid dimensions */
#define TILE_COUNT_X (32)
#define TILE_COUNT_Y (18)
#define TILE_COUNT_Z (48)
#define TOTAL_TILES (TILE_COUNT_X * TILE_COUNT_Y * TILE_COUNT_Z)
/* Compute shader workgroup sizes */
#define WORKGROUP_SIZE_X (4)
#define WORKGROUP_SIZE_Y (2)
#define WORKGROUP_SIZE_Z (4)
/* Dispatch sizes = ceil(tile_count / workgroup_size) */
#define DISPATCH_SIZE_X \
((TILE_COUNT_X + WORKGROUP_SIZE_X - 1) / WORKGROUP_SIZE_X)
#define DISPATCH_SIZE_Y \
((TILE_COUNT_Y + WORKGROUP_SIZE_Y - 1) / WORKGROUP_SIZE_Y)
#define DISPATCH_SIZE_Z \
((TILE_COUNT_Z + WORKGROUP_SIZE_Z - 1) / WORKGROUP_SIZE_Z)
/* Light constants */
#define MAX_LIGHT_COUNT (1024)
#define MAX_LIGHTS_PER_CLUSTER (100)
#define LIGHT_FLOAT_COUNT (8)
#define LIGHT_BYTE_SIZE (LIGHT_FLOAT_COUNT * 4)
/* Cluster lights buffer size:
* (8 * TOTAL_TILES) + (4 * MAX_LIGHTS_PER_CLUSTER * TOTAL_TILES) + 4 */
#define CLUSTER_LIGHTS_SIZE \
((8u * TOTAL_TILES) + (4u * MAX_LIGHTS_PER_CLUSTER * TOTAL_TILES) + 4u)
/* Uniform sizes (matching JS version) */
#define PROJECTION_UNIFORMS_SIZE (144)
#define VIEW_UNIFORMS_SIZE (80)
/* GL type constants for glTF parsing (reuse from gl.h included via GLFW) */
#ifndef GL_BYTE
#define GL_BYTE 0x1400
#define GL_UNSIGNED_BYTE 0x1401
#define GL_SHORT 0x1402
#define GL_UNSIGNED_SHORT 0x1403
#define GL_UNSIGNED_INT 0x1405
#define GL_FLOAT 0x1406
#define GL_TRIANGLES 0x0004
#define GL_TRIANGLE_STRIP 0x0005
#define GL_NEAREST 0x2600
#define GL_LINEAR 0x2601
#define GL_LINEAR_MIPMAP_NEAREST 0x2701
#define GL_NEAREST_MIPMAP_LINEAR 0x2702
#define GL_LINEAR_MIPMAP_LINEAR 0x2703
#define GL_REPEAT 0x2901
#define GL_MIRRORED_REPEAT 0x8370
#endif
/* glTF attribute locations */
#define ATTRIB_MAP_POSITION (1)
#define ATTRIB_MAP_NORMAL (2)
#define ATTRIB_MAP_TANGENT (3)
#define ATTRIB_MAP_TEXCOORD_0 (4)
#define ATTRIB_MAP_COLOR_0 (5)
/* Max scene limits */
#define MAX_PRIMITIVES (512)
#define MAX_MATERIALS (64)
#define MAX_IMAGES (128)
#define MAX_SAMPLERS (16)
#define MAX_BUFFER_VIEWS (512)
#define MAX_VERTEX_BUFFERS_PER_PRIM (4)
#define MAX_CACHED_PIPELINES (128)
/* Async GLB file loading */
#define GLB_FETCH_BUFFER_SIZE (16 * 1024 * 1024) /* 16 MB fetch buffer */
#define GLB_MODEL_PATH "assets/models/Sponza/glb/Sponza.glb"
#define IMAGES_PER_FRAME \
(8) /* images to decode per frame during progressive loading */
#define MAX_ASYNC_PIPELINES (16) /* max unique pipelines to pre-warm */
/* Output type enum */
typedef enum output_type_t {
OUTPUT_NAIVE_FORWARD = 0,
OUTPUT_DEPTH = 1,
OUTPUT_DEPTH_SLICE = 2,
OUTPUT_CLUSTER_DIST = 3,
OUTPUT_LIGHTS_PER_CLST = 4,
OUTPUT_CLUSTERED_FWD = 5,
OUTPUT_COUNT = 6,
} output_type_t;
/* GLB loading state */
typedef enum glb_load_state_t {
GLB_LOAD_PENDING = 0, /* GLB file is being fetched asynchronously */
GLB_LOAD_READY = 1, /* GLB data received, needs parsing + geometry */
GLB_LOAD_PIPELINES = 2, /* Async pipeline compilation in progress */
GLB_LOAD_TEXTURES = 3, /* Geometry loaded, decoding images progressively */
GLB_LOAD_DONE = 4, /* Scene fully created from GLB data */
} glb_load_state_t;
/* -------------------------------------------------------------------------- *
* GLTF types
* -------------------------------------------------------------------------- */
typedef struct gltf_image_t {
WGPUTexture texture;
WGPUTextureView view;
bool loaded;
} gltf_image_t;
typedef struct gltf_sampler_t {
WGPUSampler gpu_sampler;
} gltf_sampler_t;
typedef struct gltf_material_t {
float base_color_factor[4];
float metallic_factor;
float roughness_factor;
float emissive_factor[3];
float occlusion_strength;
int32_t base_color_tex_idx;
int32_t normal_tex_idx;
int32_t metallic_roughness_tex_idx;
int32_t occlusion_tex_idx;
int32_t emissive_tex_idx;
int32_t sampler_idx;
bool blend;
bool double_sided;
WGPUBuffer uniform_buffer;
WGPUBindGroup bind_group;
} gltf_material_t;
typedef struct vertex_attribute_t {
WGPUVertexFormat format;
uint32_t shader_location;
uint32_t offset;
uint32_t byte_offset; /* Original byte offset in buffer view */
} vertex_attribute_t;
typedef struct vertex_buffer_layout_t {
uint32_t buffer_view_idx;
uint32_t array_stride;
uint32_t min_byte_offset;
vertex_attribute_t attributes[8];
uint32_t attribute_count;
} vertex_buffer_layout_t;
typedef struct gltf_primitive_t {
vertex_buffer_layout_t vertex_buffers[MAX_VERTEX_BUFFERS_PER_PRIM];
uint32_t vertex_buffer_count;
uint32_t index_buffer_view_idx;
WGPUIndexFormat index_format;
uint32_t index_byte_offset;
uint32_t element_count;
int32_t material_idx;
mat4 world_matrix;
WGPUBindGroup model_bind_group;
/* Attribute flags */
bool has_normals;
bool has_tangents;
bool has_texcoords;
bool has_colors;
WGPUPrimitiveTopology topology;
} gltf_primitive_t;
/* Pipeline cache key: identifies a unique render pipeline configuration */
typedef struct pipeline_cache_key_t {
uint32_t vertex_buffer_count;
uint32_t attribute_locations; /* bitmask of shader locations present */
uint32_t array_strides[MAX_VERTEX_BUFFERS_PER_PRIM];
bool blend;
bool double_sided;
WGPUPrimitiveTopology topology;
output_type_t output_type;
} pipeline_cache_key_t;
typedef struct pipeline_cache_entry_t {
pipeline_cache_key_t key;
WGPURenderPipeline pipeline;
bool valid;
} pipeline_cache_entry_t;
typedef struct gltf_buffer_view_t {
uint32_t byte_offset; /* offset within GLB binary chunk */
uint32_t byte_length;
uint32_t byte_stride;
bool is_vertex;
bool is_index;
} gltf_buffer_view_t;
/* Light struct (mirrors JS Light class) */
typedef struct light_t {
float position[4]; /* xyz + range in w */
float color[4]; /* rgb + intensity */
float velocity[3];
float destination[3];
float travel_time;
} light_t;
/* -------------------------------------------------------------------------- *
* State struct
* -------------------------------------------------------------------------- */
static struct {
/* GLTF scene data */
struct {
gltf_buffer_view_t buffer_views[MAX_BUFFER_VIEWS];
uint32_t buffer_view_count;
WGPUBuffer
geometry_buffer; /* single GPU buffer for all vertex/index data */
gltf_image_t images[MAX_IMAGES];
uint32_t image_count;
gltf_sampler_t samplers[MAX_SAMPLERS];
uint32_t sampler_count;
gltf_material_t materials[MAX_MATERIALS];
uint32_t material_count;
gltf_primitive_t primitives[MAX_PRIMITIVES];
uint32_t primitive_count;
bool loaded;
} gltf;
/* Lights */
struct {
float uniform_array[4 + LIGHT_FLOAT_COUNT * MAX_LIGHT_COUNT];
light_t lights[MAX_LIGHT_COUNT];
uint32_t light_count;
uint32_t max_light_count;
bool render_sprites;
} light_mgr;
/* Camera */
struct {
vec3 position;
vec3 angles; /* pitch, yaw */
mat4 view_mat;
mat4 rot_mat;
bool dirty;
float speed;
bool moving;
float last_x;
float last_y;
bool key_w, key_s, key_a, key_d, key_space, key_shift;
} camera;
/* Frame uniforms (projection + view) */
float frame_uniforms[16 + 16 + 2 + 2 + 16 + 4];
/* Layout:
* [0..15] projection matrix
* [16..31] inverse projection matrix
* [32..33] output size
* [34..35] z range (near, far)
* [36..51] view matrix
* [52..54] camera position
*/
/* Bind group layouts */
struct {
WGPUBindGroupLayout frame;
WGPUBindGroupLayout material;
WGPUBindGroupLayout primitive;
WGPUBindGroupLayout cluster;
} bind_group_layouts;
/* Bind groups */
struct {
WGPUBindGroup frame;
WGPUBindGroup cluster;
} bind_groups;
/* GPU buffers */
struct {
WGPUBuffer projection;
WGPUBuffer view;
WGPUBuffer lights;
WGPUBuffer cluster_lights;
WGPUBuffer cluster;
} buffers;
/* Uniform buffers for primitives */
WGPUBuffer model_buffers[MAX_PRIMITIVES];
/* Default texture views */
struct {
WGPUTexture black_tex;
WGPUTexture white_tex;
WGPUTexture blue_tex;
WGPUTextureView black;
WGPUTextureView white;
WGPUTextureView blue;
} default_textures;
WGPUSampler default_sampler;
/* Pipelines */
WGPUPipelineLayout pipeline_layout;
WGPUPipelineLayout cluster_dist_pipeline_layout;
WGPURenderPipeline light_sprite_pipeline;
WGPUComputePipeline cluster_bounds_pipeline;
WGPUComputePipeline cluster_lights_pipeline;
/* Render bundles per output type */
WGPURenderBundle render_bundles[OUTPUT_COUNT];
bool render_bundle_valid[OUTPUT_COUNT];
/* Pipeline cache */
pipeline_cache_entry_t pipeline_cache[MAX_CACHED_PIPELINES];
uint32_t pipeline_cache_count;
/* Render pass textures for MSAA and depth */
WGPUTexture msaa_texture;
WGPUTextureView msaa_view;
WGPUTexture depth_texture;
WGPUTextureView depth_view;
/* Cluster compute bind groups */
WGPUBindGroup cluster_storage_bind_group;
WGPUBindGroupLayout cluster_storage_bgl;
/* Render pass descriptors */
WGPURenderPassColorAttachment color_attachment;
WGPURenderPassDepthStencilAttachment depth_stencil_attachment;
WGPURenderPassDescriptor render_pass_descriptor;
/* GUI settings */
struct {
output_type_t output_type;
bool render_light_sprites;
int32_t light_count;
float max_light_range;
} settings;
/* Timing */
uint64_t last_frame_time;
int32_t frame_count;
/* GLB async loading */
struct {
glb_load_state_t state;
uint8_t* data; /* pointer into glb_fetch_buffer (no malloc) */
size_t data_size;
cgltf_data* cgltf; /* parsed GLB — kept alive for image buffer refs */
uint32_t next_image_idx; /* next image to decode progressively */
} glb_load;
/* Async pipeline pre-warming — pipelines compile in background while
* sprites continue to render. Callbacks fire during ProcessEvents. */
struct {
pipeline_cache_key_t keys[MAX_ASYNC_PIPELINES];
uint32_t total;
uint32_t completed;
} async_pipelines;
uint8_t glb_fetch_buffer[GLB_FETCH_BUFFER_SIZE];
/* Render bundle descriptor */
WGPURenderBundleEncoderDescriptor render_bundle_desc;
WGPUTextureFormat context_format;
WGPUBool initialized;
} state = {
.color_attachment = {
.loadOp = WGPULoadOp_Clear,
.storeOp = WGPUStoreOp_Discard,
.clearValue = {0.0, 0.0, 0.5, 1.0},
.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED,
},
.depth_stencil_attachment = {
.depthLoadOp = WGPULoadOp_Clear,
.depthStoreOp = WGPUStoreOp_Discard,
.depthClearValue = 1.0f,
},
.render_pass_descriptor = {
.colorAttachmentCount = 1,
.colorAttachments = &state.color_attachment,
.depthStencilAttachment = &state.depth_stencil_attachment,
},
.settings = {
.output_type = OUTPUT_CLUSTERED_FWD,
.render_light_sprites = true,
.light_count = 128,
.max_light_range = 2.0f,
},
.camera = {
.position = {0.0f, 1.5f, 0.0f},
.speed = 3.0f,
.dirty = true,
},
.light_mgr = {
.light_count = 128,
.max_light_count = MAX_LIGHT_COUNT,
.render_sprites = true,
},
.frame_count = -1,
.glb_load = {
.state = GLB_LOAD_PENDING,
},
};
/* -------------------------------------------------------------------------- *
* Forward declarations for shader strings
* -------------------------------------------------------------------------- */
static const char* pbr_vertex_shader_wgsl;
static const char* pbr_vertex_shader_no_tangent_wgsl;
static const char* pbr_fragment_naive_wgsl = NULL;
static const char* pbr_fragment_clustered_wgsl = NULL;
static void build_pbr_fragment_shaders(void);
static const char* light_sprite_vertex_shader_wgsl;
static const char* light_sprite_fragment_shader_wgsl;
static const char* cluster_bounds_compute_shader_wgsl;
static const char* cluster_lights_compute_shader_wgsl;
static const char* depth_viz_fragment_wgsl;
static const char* depth_slice_viz_fragment_wgsl;
static const char* cluster_dist_viz_vertex_wgsl;
static const char* cluster_dist_viz_fragment_wgsl;
static const char* lights_per_cluster_viz_fragment_wgsl;
static const char* simple_vertex_shader_wgsl;
/* -------------------------------------------------------------------------- *
* Helper: random float in range [min, max]
* -------------------------------------------------------------------------- */
static float rand_float(float min_val, float max_val)
{
return min_val + ((float)rand() / (float)RAND_MAX) * (max_val - min_val);
}
/* -------------------------------------------------------------------------- *
* Camera
* -------------------------------------------------------------------------- */
static void camera_init(void)
{
glm_vec3_copy((vec3){0.0f, 1.5f, 0.0f}, state.camera.position);
glm_vec3_zero(state.camera.angles);
glm_mat4_identity(state.camera.view_mat);
glm_mat4_identity(state.camera.rot_mat);
state.camera.dirty = true;
state.camera.speed = 3.0f;
state.camera.key_w = false;
state.camera.key_s = false;
state.camera.key_a = false;
state.camera.key_d = false;
state.camera.key_space = false;
state.camera.key_shift = false;
/* Initial rotation: face -Z (rotate yaw by -PI/2) */
state.camera.angles[1] = -(float)GLM_PI * 0.5f;
/* Build initial rotation matrix */
glm_mat4_identity(state.camera.rot_mat);
glm_rotate_y(state.camera.rot_mat, -state.camera.angles[1],
state.camera.rot_mat);
glm_rotate_x(state.camera.rot_mat, -state.camera.angles[0],
state.camera.rot_mat);
}
static void camera_rotate_view(float x_delta, float y_delta)
{
if (x_delta == 0.0f && y_delta == 0.0f) {
return;
}
state.camera.angles[1] += x_delta;
/* Keep yaw in [0, 2*PI] */
while (state.camera.angles[1] < 0.0f) {
state.camera.angles[1] += (float)GLM_PI * 2.0f;
}
while (state.camera.angles[1] >= (float)GLM_PI * 2.0f) {
state.camera.angles[1] -= (float)GLM_PI * 2.0f;
}
state.camera.angles[0] += y_delta;
/* Clamp pitch to [-PI/2, PI/2] */
if (state.camera.angles[0] < -(float)GLM_PI * 0.5f) {
state.camera.angles[0] = -(float)GLM_PI * 0.5f;
}
if (state.camera.angles[0] > (float)GLM_PI * 0.5f) {
state.camera.angles[0] = (float)GLM_PI * 0.5f;
}
/* Update rotation matrix */
glm_mat4_identity(state.camera.rot_mat);
glm_rotate_y(state.camera.rot_mat, -state.camera.angles[1],
state.camera.rot_mat);
glm_rotate_x(state.camera.rot_mat, -state.camera.angles[0],
state.camera.rot_mat);
state.camera.dirty = true;
}
static void camera_get_view_matrix(mat4 dest)
{
if (state.camera.dirty) {
glm_mat4_identity(state.camera.view_mat);
glm_rotate_x(state.camera.view_mat, state.camera.angles[0],
state.camera.view_mat);
glm_rotate_y(state.camera.view_mat, state.camera.angles[1],
state.camera.view_mat);
vec3 neg_pos;
glm_vec3_negate_to(state.camera.position, neg_pos);
glm_translate(state.camera.view_mat, neg_pos);
state.camera.dirty = false;
}
glm_mat4_copy(state.camera.view_mat, dest);
}
static void camera_update(float frame_time_ms)
{
const float speed = (state.camera.speed / 1000.0f) * frame_time_ms;
vec3 dir = {0.0f, 0.0f, 0.0f};
if (state.camera.key_w) {
dir[2] -= speed;
}
if (state.camera.key_s) {
dir[2] += speed;
}
if (state.camera.key_a) {
dir[0] -= speed;
}
if (state.camera.key_d) {
dir[0] += speed;
}
if (state.camera.key_space) {
dir[1] += speed;
}
if (state.camera.key_shift) {
dir[1] -= speed;
}
if (dir[0] != 0.0f || dir[1] != 0.0f || dir[2] != 0.0f) {
vec3 transformed;
glm_mat4_mulv3(state.camera.rot_mat, dir, 1.0f, transformed);
glm_vec3_add(state.camera.position, transformed, state.camera.position);
state.camera.dirty = true;
}
}
/* -------------------------------------------------------------------------- *
* Light Manager
* -------------------------------------------------------------------------- */
static void light_manager_init(void)
{
memset(state.light_mgr.uniform_array, 0,
sizeof(state.light_mgr.uniform_array));
/* Ambient color: (0.002, 0.002, 0.002) */
state.light_mgr.uniform_array[0] = 0.002f;
state.light_mgr.uniform_array[1] = 0.002f;
state.light_mgr.uniform_array[2] = 0.002f;
/* Light count in uniform_array[3] (as uint32) */
uint32_t count = state.light_mgr.light_count;
memcpy(&state.light_mgr.uniform_array[3], &count, sizeof(uint32_t));
/* Initialize fixed corner lights (0-3) */
light_t* l = &state.light_mgr.lights[0];
glm_vec3_copy((vec3){8.95f, 1.0f, -3.55f}, l->position);
l->position[3] = 4.0f; /* range */
glm_vec3_copy((vec3){5.0f, 1.0f, 1.0f}, l->color);
l = &state.light_mgr.lights[1];
glm_vec3_copy((vec3){8.95f, 1.0f, 3.2f}, l->position);
l->position[3] = 4.0f;
glm_vec3_copy((vec3){5.0f, 1.0f, 1.0f}, l->color);
l = &state.light_mgr.lights[2];
glm_vec3_copy((vec3){-9.65f, 1.0f, -3.55f}, l->position);
l->position[3] = 4.0f;
glm_vec3_copy((vec3){1.0f, 1.0f, 5.0f}, l->color);
l = &state.light_mgr.lights[3];
glm_vec3_copy((vec3){-9.65f, 1.0f, 3.2f}, l->position);
l->position[3] = 4.0f;
glm_vec3_copy((vec3){1.0f, 1.0f, 5.0f}, l->color);
/* Light 4: large, bright wandering light */
l = &state.light_mgr.lights[4];
glm_vec3_copy((vec3){0.0f, 1.5f, 0.0f}, l->position);
l->position[3] = 5.0f;
glm_vec3_copy((vec3){5.0f, 5.0f, 5.0f}, l->color);
/* Initialize remaining lights randomly */
for (uint32_t i = 5; i < MAX_LIGHT_COUNT; ++i) {
l = &state.light_mgr.lights[i];
l->position[0] = rand_float(-11.0f, 10.0f);
l->position[1] = rand_float(0.2f, 6.5f);
l->position[2] = rand_float(-4.5f, 4.0f);
l->position[3] = 2.0f; /* range */
l->color[0] = rand_float(0.1f, 1.0f);
l->color[1] = rand_float(0.1f, 1.0f);
l->color[2] = rand_float(0.1f, 1.0f);
l->color[3] = 1.0f;
}
}
/* Sync lights[] into uniform_array for GPU upload */
static void light_manager_update_uniform_array(void)
{
uint32_t count = state.light_mgr.light_count;
memcpy(&state.light_mgr.uniform_array[3], &count, sizeof(uint32_t));
for (uint32_t i = 0; i < count; ++i) {
light_t* l = &state.light_mgr.lights[i];
float* dst = &state.light_mgr.uniform_array[4 + i * LIGHT_FLOAT_COUNT];
dst[0] = l->position[0];
dst[1] = l->position[1];
dst[2] = l->position[2];
dst[3] = l->position[3]; /* range */
dst[4] = l->color[0];
dst[5] = l->color[1];
dst[6] = l->color[2];
dst[7] = l->color[3]; /* intensity */
}
}
/* Update wandering lights */
static void update_wandering_lights(float time_delta)
{
for (uint32_t i = 4; i < state.light_mgr.light_count; ++i) {
light_t* l = &state.light_mgr.lights[i];
l->travel_time -= time_delta;
if (l->travel_time <= 0.0f) {
l->travel_time = rand_float(500.0f, 2000.0f);
l->destination[0] = rand_float(-11.0f, 10.0f);
l->destination[1] = rand_float(0.2f, 6.5f);
l->destination[2] = rand_float(-4.5f, 4.0f);
}
l->velocity[0]
+= (l->destination[0] - l->position[0]) * 0.000005f * time_delta;
l->velocity[1]
+= (l->destination[1] - l->position[1]) * 0.000005f * time_delta;
l->velocity[2]
+= (l->destination[2] - l->position[2]) * 0.000005f * time_delta;
/* Clamp velocity */
float vel_len = glm_vec3_norm(l->velocity);
if (vel_len > 0.05f) {
glm_vec3_scale(l->velocity, 0.05f / vel_len, l->velocity);
}
glm_vec3_add(l->position, l->velocity, l->position);
}
}
static void update_light_range(float range)
{
for (uint32_t i = 5; i < state.light_mgr.max_light_count; ++i) {
state.light_mgr.lights[i].position[3] = range;
}
}
/* -------------------------------------------------------------------------- *
* Default textures (1x1 pixel solid colors)
* -------------------------------------------------------------------------- */
static void create_default_textures(wgpu_context_t* wgpu_context)
{
WGPUDevice device = wgpu_context->device;
/* Helper to create a 1x1 texture */
uint8_t black_pixel[4] = {0, 0, 0, 0};
uint8_t white_pixel[4] = {255, 255, 255, 255};
uint8_t blue_pixel[4] = {0, 0, 255, 0};
WGPUTextureDescriptor tex_desc = {
.label = STRVIEW("Default texture"),
.usage = WGPUTextureUsage_CopyDst | WGPUTextureUsage_TextureBinding,
.dimension = WGPUTextureDimension_2D,
.size = {1, 1, 1},
.format = WGPUTextureFormat_RGBA8Unorm,
.mipLevelCount = 1,
.sampleCount = 1,
};
state.default_textures.black_tex = wgpuDeviceCreateTexture(device, &tex_desc);
state.default_textures.white_tex = wgpuDeviceCreateTexture(device, &tex_desc);
state.default_textures.blue_tex = wgpuDeviceCreateTexture(device, &tex_desc);
WGPUExtent3D size = {1, 1, 1};
wgpu_image_to_texure(wgpu_context, state.default_textures.black_tex,
black_pixel, size, 4);
wgpu_image_to_texure(wgpu_context, state.default_textures.white_tex,
white_pixel, size, 4);
wgpu_image_to_texure(wgpu_context, state.default_textures.blue_tex,
blue_pixel, size, 4);
state.default_textures.black
= wgpuTextureCreateView(state.default_textures.black_tex, NULL);
state.default_textures.white
= wgpuTextureCreateView(state.default_textures.white_tex, NULL);
state.default_textures.blue
= wgpuTextureCreateView(state.default_textures.blue_tex, NULL);
/* Default sampler */
WGPUSamplerDescriptor sampler_desc = {
.label = STRVIEW("Default sampler"),
.magFilter = WGPUFilterMode_Linear,
.minFilter = WGPUFilterMode_Linear,
.mipmapFilter = WGPUMipmapFilterMode_Linear,
.addressModeU = WGPUAddressMode_Repeat,
.addressModeV = WGPUAddressMode_Repeat,
.addressModeW = WGPUAddressMode_Repeat,
.maxAnisotropy = 1,
};
state.default_sampler = wgpuDeviceCreateSampler(device, &sampler_desc);
}
/* -------------------------------------------------------------------------- *
* GLTF Loading
* -------------------------------------------------------------------------- */
static WGPUVertexFormat get_vertex_format(int component_type,
int component_count, bool normalized)
{
if (component_type == GL_FLOAT) {
switch (component_count) {
case 1:
return WGPUVertexFormat_Float32;
case 2:
return WGPUVertexFormat_Float32x2;
case 3:
return WGPUVertexFormat_Float32x3;
case 4:
return WGPUVertexFormat_Float32x4;
}
}
else if (component_type == GL_UNSIGNED_BYTE) {
if (normalized) {
switch (component_count) {
case 2:
return WGPUVertexFormat_Unorm8x2;
case 4:
return WGPUVertexFormat_Unorm8x4;
}
}
else {
switch (component_count) {
case 2:
return WGPUVertexFormat_Uint8x2;
case 4:
return WGPUVertexFormat_Uint8x4;
}
}
}
else if (component_type == GL_SHORT) {
if (normalized) {
switch (component_count) {
case 2:
return WGPUVertexFormat_Snorm16x2;
case 4:
return WGPUVertexFormat_Snorm16x4;
}
}
else {
switch (component_count) {
case 2:
return WGPUVertexFormat_Sint16x2;
case 4:
return WGPUVertexFormat_Sint16x4;
}
}
}
else if (component_type == GL_UNSIGNED_SHORT) {
if (normalized) {
switch (component_count) {
case 2:
return WGPUVertexFormat_Unorm16x2;
case 4:
return WGPUVertexFormat_Unorm16x4;
}
}
else {
switch (component_count) {
case 2:
return WGPUVertexFormat_Uint16x2;
case 4:
return WGPUVertexFormat_Uint16x4;
}
}
}
else if (component_type == GL_UNSIGNED_INT) {
switch (component_count) {
case 1:
return WGPUVertexFormat_Uint32;
case 2:
return WGPUVertexFormat_Uint32x2;
case 3:
return WGPUVertexFormat_Uint32x3;
case 4:
return WGPUVertexFormat_Uint32x4;
}
}
return WGPUVertexFormat_Float32x3; /* fallback */
}
static int get_component_count(cgltf_type type)
{
switch (type) {
case cgltf_type_scalar:
return 1;
case cgltf_type_vec2:
return 2;
case cgltf_type_vec3:
return 3;
case cgltf_type_vec4:
return 4;
case cgltf_type_mat2:
return 4;
case cgltf_type_mat3:
return 9;
case cgltf_type_mat4:
return 16;
default:
return 0;
}
}
static int get_component_type_gl(cgltf_component_type ct)
{
switch (ct) {
case cgltf_component_type_r_8:
return GL_BYTE;
case cgltf_component_type_r_8u:
return GL_UNSIGNED_BYTE;
case cgltf_component_type_r_16:
return GL_SHORT;
case cgltf_component_type_r_16u:
return GL_UNSIGNED_SHORT;
case cgltf_component_type_r_32u:
return GL_UNSIGNED_INT;
case cgltf_component_type_r_32f:
return GL_FLOAT;
default:
return GL_FLOAT;
}
}
static uint32_t get_component_type_size(int gl_type)
{
switch (gl_type) {
case GL_BYTE:
case GL_UNSIGNED_BYTE:
return 1;
case GL_SHORT:
case GL_UNSIGNED_SHORT:
return 2;
case GL_UNSIGNED_INT:
case GL_FLOAT:
return 4;
default:
return 0;
}
}
static int get_attrib_location(const char* name)
{
if (strcmp(name, "POSITION") == 0)
return ATTRIB_MAP_POSITION;
if (strcmp(name, "NORMAL") == 0)
return ATTRIB_MAP_NORMAL;
if (strcmp(name, "TANGENT") == 0)
return ATTRIB_MAP_TANGENT;
if (strcmp(name, "TEXCOORD_0") == 0)
return ATTRIB_MAP_TEXCOORD_0;
if (strcmp(name, "COLOR_0") == 0)
return ATTRIB_MAP_COLOR_0;
return -1;
}
static const char* get_attrib_name(cgltf_attribute_type type, int index)
{
switch (type) {
case cgltf_attribute_type_position:
return "POSITION";
case cgltf_attribute_type_normal:
return "NORMAL";
case cgltf_attribute_type_tangent:
return "TANGENT";
case cgltf_attribute_type_texcoord:
return (index == 0) ? "TEXCOORD_0" : NULL;
case cgltf_attribute_type_color:
return (index == 0) ? "COLOR_0" : NULL;
default:
return NULL;
}
}
static WGPUAddressMode get_address_mode(int wrap)
{
switch (wrap) {
case GL_REPEAT:
return WGPUAddressMode_Repeat;
case GL_MIRRORED_REPEAT:
return WGPUAddressMode_MirrorRepeat;
default:
return WGPUAddressMode_ClampToEdge;
}
}
/* Forward declaration for node processing */
static void process_gltf_node(wgpu_context_t* wgpu_context, cgltf_data* data,
cgltf_node* node, mat4 parent_matrix);
/* (Re)create a material's bind group using currently loaded textures.
* Called initially with default textures, then again when images arrive. */
static void recreate_material_bind_group(WGPUDevice device, uint32_t mat_idx)
{
gltf_material_t* g_mat = &state.gltf.materials[mat_idx];
/* Get sampler */
WGPUSampler sampler = state.default_sampler;
if (g_mat->sampler_idx >= 0
&& g_mat->sampler_idx < (int32_t)state.gltf.sampler_count) {
sampler = state.gltf.samplers[g_mat->sampler_idx].gpu_sampler;
}
/* Get texture views — use default if image not yet loaded */
WGPUTextureView base_color_view = state.default_textures.white;
if (g_mat->base_color_tex_idx >= 0
&& g_mat->base_color_tex_idx < (int32_t)state.gltf.image_count
&& state.gltf.images[g_mat->base_color_tex_idx].loaded) {
base_color_view = state.gltf.images[g_mat->base_color_tex_idx].view;
}
WGPUTextureView normal_view = state.default_textures.blue;
if (g_mat->normal_tex_idx >= 0
&& g_mat->normal_tex_idx < (int32_t)state.gltf.image_count
&& state.gltf.images[g_mat->normal_tex_idx].loaded) {
normal_view = state.gltf.images[g_mat->normal_tex_idx].view;
}
WGPUTextureView mr_view = state.default_textures.white;
if (g_mat->metallic_roughness_tex_idx >= 0
&& g_mat->metallic_roughness_tex_idx < (int32_t)state.gltf.image_count
&& state.gltf.images[g_mat->metallic_roughness_tex_idx].loaded) {
mr_view = state.gltf.images[g_mat->metallic_roughness_tex_idx].view;
}
WGPUTextureView occ_view = state.default_textures.white;
if (g_mat->occlusion_tex_idx >= 0
&& g_mat->occlusion_tex_idx < (int32_t)state.gltf.image_count
&& state.gltf.images[g_mat->occlusion_tex_idx].loaded) {
occ_view = state.gltf.images[g_mat->occlusion_tex_idx].view;
}
WGPUTextureView em_view = state.default_textures.black;
if (g_mat->emissive_tex_idx >= 0
&& g_mat->emissive_tex_idx < (int32_t)state.gltf.image_count