techkaczynski.dev / code

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app.c4
-rw-r--r--app.h1
-rw-r--r--editor.c672
-rw-r--r--editor.h3
-rw-r--r--ent.c104
-rw-r--r--ent.h24
-rw-r--r--env.c31
-rw-r--r--res.c32
-rw-r--r--res.h3
-rw-r--r--save.c153
10 files changed, 862 insertions, 165 deletions
diff --git a/app.c b/app.c
index 851b179..c1010d4 100644
--- a/app.c
+++ b/app.c
@@ -49,6 +49,8 @@ app_exit(void) {
static void
app_loop(void) {
app.run = !WindowShouldClose();
+ f64 frame_start = GetTime();
+
entity_system_update();
/* this is some hypothetical bullshit, probably will change */
@@ -77,6 +79,8 @@ app_loop(void) {
}
editor_draw_ui();
+
+ app.frame_time_ms = (GetTime() - frame_start) * 1000.0f;
EndDrawing();
}
diff --git a/app.h b/app.h
index 73d85d4..1f0ff23 100644
--- a/app.h
+++ b/app.h
@@ -15,6 +15,7 @@ typedef enum {
typedef struct {
int w, h;
+ f32 frame_time_ms;
AppMode mode;
AppFlag flags;
bool run;
diff --git a/editor.c b/editor.c
index 9f5eb21..ab70611 100644
--- a/editor.c
+++ b/editor.c
@@ -39,18 +39,45 @@ EditorInfo ed = {
};
typedef enum {
- MDL_PICK_COMP_ADD,
- MDL_PICK_COMP_UPDATE,
- MDL_PICK_ENT_CREATE,
-} ModelPickerMode;
+ ASSET_PICK_MDL_COMP_ADD,
+ ASSET_PICK_MDL_COMP_UPDATE,
+ ASSET_PICK_MDL_ENT_CREATE,
+ ASSET_PICK_MAT_TEX,
+} AssetPickerMode;
+
+typedef struct {
+ cstr key;
+ int score;
+ Asset *asset;
+ bool tried_loading;
+} AssetChoice;
+
+static struct {
+ RenderTexture preview;
+ AssetChoice *items;
+ int item_cnt, match_cnt, sel_idx, scroll;
+ AssetTag tag;
+ AssetPickerMode mode;
+ u32 mat_idx; // material index dummy
+ char query[128];
+} asset_picker;
+
+static const cstr material_shader_keys[] = {
+ "model",
+ "model_cutoff"
+};
+
+static const char material_shader_labels[] =
+ "Model;Model (cutoff)";
/* forward decl */
static bool selection_root(Entity *ent);
static void action_apply(EditorAction*, bool);
static void action_free(EditorAction*);
-static void model_picker_open(ModelPickerMode mode);
-static void model_picker_close(void);
+static void asset_picker_open(AssetTag tag, AssetPickerMode mode);
+static void asset_picker_close(void);
static void physics_pick(void);
+static void editor_set_material(cstr key, u32 mat_idx, MaterialProperty prop);
static void
history_push(EditorAction action) {
@@ -200,11 +227,16 @@ action_begin(EditorActionTag tag, bool roots_only) {
return action;
case ED_ACTION_DELETE:
+ case ED_ACTION_DUPLICATE:
recursive = true;
break;
case ED_ACTION_RENAME:
+ break;
+
case ED_ACTION_MODEL:
+ break;
+
case ED_ACTION_PHYSICS:
break;
@@ -267,14 +299,16 @@ action_begin(EditorActionTag tag, bool roots_only) {
break;
}
+ case ED_ACTION_DUPLICATE:
+ break;
+
case ED_ACTION_RENAME:
- // Name ownership will transfer when the rename is submitted.
+ // Name ownership will transfer when the rename is
+ // submitted.
break;
case ED_ACTION_MODEL:
- before->model = ent->mdl;
- before->model.key = ent->mdl.key ?
- strdup(ent->mdl.key) : NULL;
+ before->model = entity_model_copy(&ent->mdl);
break;
case ED_ACTION_PHYSICS:
@@ -317,6 +351,7 @@ action_apply(EditorAction *action, bool undo) {
break;
case ED_ACTION_CREATE:
+ case ED_ACTION_DUPLICATE:
if (undo) {
action_entity_extract(action);
} else {
@@ -381,6 +416,7 @@ action_free(EditorAction *action) {
case ED_ACTION_CREATE:
case ED_ACTION_DELETE:
+ case ED_ACTION_DUPLICATE:
for (u32 i = 0; i < action->item_cnt; ++i) {
entity_clean(&action->item[i].before.stored.ent);
}
@@ -394,7 +430,7 @@ action_free(EditorAction *action) {
case ED_ACTION_MODEL:
for (u32 i = 0; i < action->item_cnt; ++i) {
- free(action->item[i].before.model.key);
+ entity_model_free(&action->item[i].before.model);
}
break;
@@ -446,6 +482,7 @@ action_commit(EditorAction *action) {
case ED_ACTION_CREATE:
case ED_ACTION_DELETE:
+ case ED_ACTION_DUPLICATE:
changed = action->item_cnt != 0;
break;
@@ -479,6 +516,27 @@ action_commit(EditorAction *action) {
strcmp(before->key ? before->key : "",
after->key ? after->key : "") != 0;
+ if (before->mat_cnt != after->mat_cnt) {
+ changed = true;
+ continue;
+ }
+
+ for (u32 j = 0; j < before->mat_cnt; ++j) {
+ const MaterialOverride *a, *b;
+ a = &before->materials[j];
+ b = &after->materials[j];
+
+ changed |= strcmp(a->tex_key ?
+ a->tex_key : "",
+ b->tex_key ?
+ b->tex_key : "") != 0;
+
+ changed |= strcmp(a->shader_key ?
+ a->shader_key : "",
+ b->shader_key ?
+ b->shader_key :
+ "") != 0;
+ }
}
break;
@@ -532,7 +590,7 @@ editor_init(void) {
void
editor_exit(void) {
- model_picker_close();
+ asset_picker_close();
free(rot_record);
action_free(&tr_action);
tr_action = (EditorAction){0};
@@ -621,6 +679,86 @@ editor_focus_selection(void) {
cam_focus(center, distance, view_height);
}
+static void
+editor_duplicate(void) {
+ EditorAction action;
+ action = action_begin(ED_ACTION_DUPLICATE, true);
+
+ if (!action.item_cnt) {
+ action_free(&action);
+ return;
+ }
+
+ u64 *copies = calloc(action.item_cnt, sizeof(*copies));
+ if (!copies) {
+ perror("editor_duplicate: alloc failed.");
+ exit(1);
+ }
+
+ for (u32 i = 0; i < action.item_cnt; ++i) {
+ copies[i] = entity_create(false);
+
+ /* Creation may reallocate the entity array. */
+ u64 source_id = entity_id_from_pid(action.item[i].pid);
+ Entity *source = entity_get(source_id);
+ Entity *copy = entity_get(copies[i]);
+
+ free(copy->name);
+ copy->name = source->name ? strdup(source->name) : NULL;
+
+ copy->tr.pos = source->tr.pos;
+ copy->tr.rot = source->tr.rot;
+ copy->tr.scale = source->tr.scale;
+
+ copy->mdl = entity_model_copy(&source->mdl);
+ copy->phys = entity_physics_copy(&source->phys);
+
+ copy->ed.selected = source->ed.selected;
+ source->ed.selected = false;
+ }
+
+ /*
+ * Collection visits children first. Linking in reverse order
+ * preserves sibling order when set_parent prepends children.
+ */
+ for (u32 n = action.item_cnt; n > 0; --n) {
+ u32 i = n - 1;
+ u64 source_id = entity_id_from_pid(action.item[i].pid);
+ Entity *source = entity_get(source_id);
+
+ u64 parent_id = source->tr.parent;
+ Entity *parent = entity_get(parent_id);
+ u64 parent_pid = parent ? parent->pid : 0;
+
+ for (u32 j = 0; j < action.item_cnt; ++j) {
+ if (action.item[j].pid == parent_pid) {
+ parent_id = copies[j];
+ break;
+ }
+ }
+
+ entity_set_parent(copies[i], parent_id);
+ }
+
+ /* Record the completed hierarchy for undo and redo. */
+ for (u32 i = 0; i < action.item_cnt; ++i) {
+ Entity *copy = entity_get(copies[i]);
+ Entity *parent = entity_get(copy->tr.parent);
+ Entity *prev = entity_get(copy->tr.prev);
+ Entity *next = entity_get(copy->tr.next);
+
+ EditorActionData *data = &action.item[i].before;
+
+ action.item[i].pid = copy->pid;
+ data->stored.parent = parent ? parent->pid : 0;
+ data->stored.prev = prev ? prev->pid : 0;
+ data->stored.next = next ? next->pid : 0;
+ }
+
+ free(copies);
+ action_commit(&action);
+}
+
void
editor_update(void) {
bool ctrl = IsKeyDown(KEY_LEFT_CONTROL);
@@ -663,8 +801,8 @@ case ED_INTERACT_NONE:
}
if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT) ||
- (main_cam.state == CAM_MODE_EDIT_ORBIT &&
- IsMouseButtonPressed(MOUSE_BUTTON_MIDDLE))) {
+ (main_cam.state == CAM_MODE_EDIT_ORBIT &&
+ IsMouseButtonPressed(MOUSE_BUTTON_MIDDLE))) {
ed.interact = ED_INTERACT_CAMERA;
main_cam.update = false;
DisableCursor();
@@ -682,6 +820,9 @@ case ED_INTERACT_NONE:
break;
}
+ if (!ctrl && !shift && !alt && IsKeyPressed(KEY_C)) {
+ ed.draw_physics = !ed.draw_physics;
+ }
if (!ctrl && IsKeyPressed(KEY_F)) {
editor_focus_selection();
@@ -728,6 +869,12 @@ case ED_INTERACT_NONE:
}
}
+ if (ctrl && IsKeyPressed(KEY_D) && ed.sel_cnt) {
+ editor_duplicate();
+ ed.interact = ED_INTERACT_MOVE;
+ break;
+ }
+
if (IsKeyPressed(KEY_DELETE)) {
if (!ctrl && ed.sel_cnt) {
EditorAction action = action_begin(ED_ACTION_DELETE, true);
@@ -1101,8 +1248,11 @@ editor_move(void) {
}
}
+ bool reset = false;
+
if (IsKeyPressed(KEY_L)) {
main_cam.local_space = !main_cam.local_space;
+ reset = true;
}
bool local = main_cam.local_space && ed.sel_cnt == 1;
@@ -1113,6 +1263,7 @@ editor_move(void) {
continue;
}
+ reset = true;
bool plane = IsKeyDown(KEY_LEFT_SHIFT);
if (ed.move_axis == i && ed.move_plane == plane) {
@@ -1155,11 +1306,17 @@ editor_move(void) {
bool use_local = local && ed.move_axis >= 0 &&
!IsKeyDown(KEY_LEFT_ALT);
- if (!Vector3Equals(axis, previous_axis) ||
- plane != previous_plane || use_local != previous_local) {
+ if (reset || !Vector3Equals(axis, previous_axis) ||
+ plane != previous_plane ||
+ use_local != previous_local) {
+ if (reset) {
+ action_apply(&tr_action, true);
+ }
+
previous_axis = axis;
previous_plane = plane;
previous_local = use_local;
+
raw_pivot = selected_pivot();
ed.move_snap_origin = raw_pivot;
started = false;
@@ -2012,6 +2169,12 @@ panel_properties(EditorPanelContainer container, bool input) {
static PanelEdit edit;
static bool add_open;
static int add_choice;
+ static int mat_idx;
+ static bool shader_open;
+
+ Rect shader_bounds = {0};
+ bool shader_visible = false;
+ int shader_choice = 0;
Rect bounds = ed.panel[container].bounds;
int state = GuiGetState();
@@ -2026,10 +2189,11 @@ panel_properties(EditorPanelContainer container, bool input) {
if (!input) {
add_open = false;
edit.focus = 0;
+ shader_open = false;
GuiSetState(STATE_DISABLED);
}
- if (add_open) {
+ if (add_open || shader_open) {
GuiLock();
}
@@ -2217,8 +2381,27 @@ panel_properties(EditorPanelContainer container, bool input) {
goto physics_comp;
}
+ int material_count = 0;
+ int first_material = 0;
+
+ if (ent) {
+ Asset *asset = asset_get(model->key);
+ if (asset) {
+ Model *mdl = &asset->mdl;
+ material_count = mdl->materialCount;
+ first_material = 1;
+
+ for (int i = 0; i < mdl->meshCount; ++i) {
+ if (mdl->meshMaterial[i] == 0) {
+ first_material = 0;
+ break;
+ }
+ }
+ }
+ }
+
group.y = next_y;
- group.height = 80;
+ group.height = material_count ? 170 : 80;
GuiGroupBox(group, "Model");
row = (Rect){
@@ -2230,8 +2413,7 @@ panel_properties(EditorPanelContainer container, bool input) {
GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
if (GuiButton((Rect){
- group.x, group.y + group.height + pad,
- 120, 20
+ group.x, group.y + group.height + pad, 120, 20
}, "Remove Model")) {
edit.focus = 0;
@@ -2242,8 +2424,7 @@ panel_properties(EditorPanelContainer container, bool input) {
entity_id_from_pid(action.item[i].pid)
);
- free(target->mdl.key);
- target->mdl = (ModelCmp){0};
+ entity_model_free(&target->mdl);
}
action_commit(&action);
@@ -2257,7 +2438,7 @@ panel_properties(EditorPanelContainer container, bool input) {
(Rect){row.x + 40, row.y, row.width - 40, row.height},
key_mixed ? "<multiple>" : GetFileName(model->key))) {
edit.focus = 0;
- model_picker_open(MDL_PICK_COMP_UPDATE);
+ asset_picker_open(ASSET_TAG_MODEL, ASSET_PICK_MDL_COMP_UPDATE);
}
row.y += 30;
@@ -2278,6 +2459,97 @@ panel_properties(EditorPanelContainer container, bool input) {
action_commit(&action);
}
+ if (!material_count) {
+ goto model_end;
+ }
+
+ row.y += 30;
+
+ GuiLabel((Rect){
+ row.x, row.y, 64, row.height
+ }, "Material");
+
+ Rect control = {
+ row.x + 72, row.y,
+ row.width - 72, row.height
+ };
+
+ int previous = mat_idx;
+ GuiSpinner(control, NULL, &mat_idx, first_material,
+ material_count - 1, false);
+
+ if (mat_idx != previous) {
+ edit.focus = 0;
+ }
+
+ cstr texture_key = NULL;
+
+ if ((u32)mat_idx < model->mat_cnt) {
+ texture_key = model->materials[mat_idx].tex_key;
+ }
+
+ cstr texture_label = "<model texture>";
+ if (texture_key) {
+ texture_label = GetFileName(texture_key);
+ }
+
+ row.y += 30;
+ control.y = row.y;
+
+ GuiLabel((Rect){
+ row.x, row.y, 64, row.height
+ }, "Texture");
+
+ Rect texture_button = control;
+ texture_button.width -= 56;
+
+ Rect reset_button = {
+ control.x + control.width - 52,
+ control.y, 52, control.height
+ };
+
+ if (GuiButton(texture_button, texture_label)) {
+ edit.focus = 0;
+ asset_picker_open(ASSET_TAG_TEXTURE,
+ ASSET_PICK_MAT_TEX);
+ asset_picker.mat_idx = mat_idx;
+ }
+
+ if (GuiButton(reset_button, "Reset")) {
+ edit.focus = 0;
+ editor_set_material(NULL, mat_idx, MAT_PROP_TEXTURE);
+ }
+
+ row.y += 30;
+ control.y = row.y;
+
+ GuiLabel((Rect){
+ row.x, row.y, 64, row.height
+ }, "Shader");
+
+ cstr shader_key = "model";
+
+ if ((u32)mat_idx < model->mat_cnt) {
+ cstr key = model->materials[mat_idx].shader_key;
+ if (key) {
+ shader_key = key;
+ }
+ }
+
+ int shader_count = sizeof(material_shader_keys) /
+ sizeof(material_shader_keys[0]);
+
+ for (int i = 0; i < shader_count; ++i) {
+ if (strcmp(shader_key, material_shader_keys[i]) == 0) {
+ shader_choice = i;
+ break;
+ }
+ }
+
+ shader_bounds = control;
+ shader_visible = true;
+
+ model_end:
next_y = group.y + group.height + pad + 20 + 16;
physics_comp:
@@ -2362,7 +2634,9 @@ panel_properties(EditorPanelContainer container, bool input) {
next_y = group.y + group.height + pad + 20 + 16;
comp_add:
- GuiUnlock();
+ if (!shader_open) {
+ GuiUnlock();
+ }
row = (Rect){
bounds.x + pad,
@@ -2415,7 +2689,7 @@ panel_properties(EditorPanelContainer container, bool input) {
add_choice = 0;
if (choice == model_choice) {
- model_picker_open(MDL_PICK_COMP_ADD);
+ asset_picker_open(ASSET_TAG_MODEL, ASSET_PICK_MDL_COMP_ADD);
goto reset;
}
@@ -2441,11 +2715,37 @@ panel_properties(EditorPanelContainer container, bool input) {
reset:
GuiUnlock();
+
+ if (!shader_visible) {
+ shader_open = false;
+ } else {
+ if (add_open) {
+ GuiLock();
+ }
+
+ int previous = shader_choice;
+
+ if (GuiDropdownBox(shader_bounds, material_shader_labels,
+ &shader_choice, shader_open)) {
+ shader_open = !shader_open;
+ edit.focus = 0;
+
+ if (!shader_open && shader_choice != previous) {
+ cstr key = material_shader_keys[shader_choice];
+
+ editor_set_material(key, mat_idx,
+ MAT_PROP_SHADER);
+ }
+ }
+
+ GuiUnlock();
+ }
+
GuiSetState(state);
GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT);
if (input && container != ED_PNL_CENTER) {
ed.focus_captured = edit.focus != 0 ||
- add_open ||
+ add_open || shader_open ||
ed.panel[ED_PNL_CENTER].enabled;
}
}
@@ -2491,23 +2791,9 @@ panel_space(EditorPanelContainer container) {
}
/* model picker */
-typedef struct {
- cstr key;
- int score;
- Asset *asset;
- bool tried_loading;
-} ModelChoice;
-
-static struct {
- RenderTexture preview;
- ModelChoice *items;
- int item_cnt, match_cnt, sel_idx, scroll;
- ModelPickerMode mode;
- char query[128];
-} model_picker;
static bool
-editor_set_model(cstr key, ModelPickerMode mode) {
+editor_set_model(cstr key, AssetPickerMode mode) {
Asset *asset = asset_get(key);
if (!asset || asset->tag != ASSET_TAG_MODEL) {
return false;
@@ -2519,29 +2805,94 @@ editor_set_model(cstr key, ModelPickerMode mode) {
u64 id = entity_id_from_pid(action.item[i].pid);
Entity *ent = entity_get(id);
- if ((mode == MDL_PICK_COMP_ADD && ent->mdl.has) ||
- (mode == MDL_PICK_COMP_UPDATE && !ent->mdl.has)) {
+ if ((mode == ASSET_PICK_MDL_COMP_ADD&& ent->mdl.has) ||
+ (mode == ASSET_PICK_MDL_COMP_UPDATE &&
+ !ent->mdl.has)) {
continue;
}
- free(ent->mdl.key);
+ bool draw = ent->mdl.has ? ent->mdl.draw : true;
+ entity_model_free(&ent->mdl);
ent->mdl = (ModelCmp){
- .key = strdup(key),
- .mdl = &asset->mdl,
- .has = true,
- .draw = ent->mdl.has ? ent->mdl.draw : true,
- };
+ .key = strdup(key),
+ .mdl = &asset->mdl,
+ .has = true,
+ .draw = draw,
+ };
}
action_commit(&action);
return true;
}
+static void
+editor_set_material(cstr key, u32 mat_idx, MaterialProperty prop) {
+ EditorAction action = action_begin(ED_ACTION_MODEL, false);
+
+ for (u32 i = 0; i < action.item_cnt; ++i) {
+ u64 id = entity_id_from_pid(action.item[i].pid);
+ Entity *ent = entity_get(id);
+ ModelCmp *model = &ent->mdl;
+
+ if (!model->has) {
+ continue;
+ }
+
+ if (!key && mat_idx >= model->mat_cnt) {
+ continue;
+ }
+
+ if (!model->mdl) {
+ Asset *asset = asset_get(model->key);
+ if (!asset) {
+ continue;
+ }
+ model->mdl = &asset->mdl;
+ }
+
+ if (mat_idx >= (u32)model->mdl->materialCount) {
+ continue;
+ }
+
+ if (mat_idx >= model->mat_cnt) {
+ u32 count = model->mdl->materialCount;
+ MaterialOverride *materials;
+
+ materials = realloc(model->materials,
+ count * sizeof(*materials));
+
+ if (!materials) {
+ perror("editor_set_texture: alloc failed.");
+ exit(1);
+ }
+
+ memset(materials + model->mat_cnt, 0,
+ (count - model->mat_cnt) *
+ sizeof(*materials));
+
+ model->materials = materials;
+ model->mat_cnt = count;
+ }
+
+ MaterialOverride *material;
+ material = &model->materials[mat_idx];
+
+ char **field = prop == MAT_PROP_TEXTURE ?
+ &material->tex_key : &material->shader_key;
+
+ char *replacement = key ? strdup(key) : NULL;
+ free(*field);
+ *field = replacement;
+ }
+
+ action_commit(&action);
+}
+
/* qsort compare func */
static int
model_choice_compare(const void *a, const void *b) {
- const ModelChoice *left = a;
- const ModelChoice *right = b;
+ const AssetChoice *left = a;
+ const AssetChoice *right = b;
if (left->score > right->score) {
return -1;
@@ -2560,12 +2911,12 @@ model_choice_compare(const void *a, const void *b) {
}
static void
-model_picker_close(void) {
- if (model_picker.preview.id) {
- UnloadRenderTexture(model_picker.preview);
+asset_picker_close(void) {
+ if (asset_picker.preview.id) {
+ UnloadRenderTexture(asset_picker.preview);
}
- free(model_picker.items);
- memset(&model_picker, 0, sizeof(model_picker));
+ free(asset_picker.items);
+ memset(&asset_picker, 0, sizeof(asset_picker));
ed.panel[ED_PNL_CENTER].enabled = false;
ed.focus = ED_PNL_NULL;
@@ -2573,31 +2924,33 @@ model_picker_close(void) {
}
static void
-model_picker_open(ModelPickerMode mode) {
- model_picker.mode = mode;
- char **keys = asset_mdl_keys(&model_picker.item_cnt);
-
- if (model_picker.item_cnt) {
- model_picker.items = calloc(model_picker.item_cnt,
- sizeof(*model_picker.items));
- if (!model_picker.items) {
- perror("model_picker_open: alloc failed.");
+asset_picker_open(AssetTag tag, AssetPickerMode mode) {
+ asset_picker.tag = tag;
+ asset_picker.mode = mode;
+
+ char **keys = asset_type_keys(tag, &asset_picker.item_cnt);
+ if (asset_picker.item_cnt) {
+ asset_picker.items = calloc(asset_picker.item_cnt,
+ sizeof(*asset_picker.items));
+ if (!asset_picker.items) {
+ perror("asset_picker_open: alloc failed.");
exit(1);
}
}
- for (int i = 0; i < model_picker.item_cnt; ++i) {
- model_picker.items[i].key = keys[i];
+ for (int i = 0; i < asset_picker.item_cnt; ++i) {
+ asset_picker.items[i].key = keys[i];
}
free(keys);
- model_picker.match_cnt = model_picker.item_cnt;
- if (model_picker.item_cnt > 1) {
- qsort(model_picker.items, model_picker.item_cnt,
- sizeof(*model_picker.items), model_choice_compare);
+
+ asset_picker.match_cnt = asset_picker.item_cnt;
+ if (asset_picker.item_cnt > 1) {
+ qsort(asset_picker.items, asset_picker.item_cnt,
+ sizeof(*asset_picker.items), model_choice_compare);
}
- ed.panel[ED_PNL_CENTER].type = ED_PNL_TYPE_MODEL_PICKER;
+ ed.panel[ED_PNL_CENTER].type = ED_PNL_TYPE_ASSET_PICKER;
ed.panel[ED_PNL_CENTER].enabled = true;
ed.focus = ED_PNL_CENTER;
ed.focus_captured = true;
@@ -2605,13 +2958,13 @@ model_picker_open(ModelPickerMode mode) {
static void
model_picker_preview(Rect bounds, Model mdl) {
- if (model_picker.preview.texture.width != bounds.width ||
- model_picker.preview.texture.height != bounds.height) {
- if (model_picker.preview.texture.id) {
- UnloadRenderTexture(model_picker.preview);
+ if (asset_picker.preview.texture.width != bounds.width ||
+ asset_picker.preview.texture.height != bounds.height) {
+ if (asset_picker.preview.texture.id) {
+ UnloadRenderTexture(asset_picker.preview);
}
- model_picker.preview =
+ asset_picker.preview =
LoadRenderTexture(bounds.width, bounds.height);
}
@@ -2649,7 +3002,7 @@ model_picker_preview(Rect bounds, Model mdl) {
.projection = CAMERA_PERSPECTIVE,
};
- BeginTextureMode(model_picker.preview);
+ BeginTextureMode(asset_picker.preview);
ClearBackground((Color){ 35, 35, 40, 255 });
BeginMode3D(cam);
@@ -2666,20 +3019,41 @@ model_picker_preview(Rect bounds, Model mdl) {
EndMode3D();
EndTextureMode();
- DrawTexturePro(model_picker.preview.texture, (Rect) {
+ DrawTexturePro(asset_picker.preview.texture, (Rect) {
0, 0, bounds.width, -bounds.height,
}, bounds, (Vec2){0}, 0, WHITE);
}
+static void
+texture_picker_preview(Rect bounds, Texture tex) {
+ if (bounds.width <= 0 || bounds.height <= 0) {
+ return;
+ }
+
+ f32 scale = fminf(bounds.width / tex.width,
+ bounds.height / tex.height);
+
+ Rect dest = {
+ .width = tex.width * scale,
+ .height = tex.height * scale,
+ };
+
+ dest.x = bounds.x + (bounds.width - dest.width) * 0.5f;
+ dest.y = bounds.y + (bounds.height - dest.height) * 0.5f;
+
+ DrawTexturePro(tex,
+ (Rect){0, 0, tex.width, tex.height},
+ dest, (Vec2){0}, 0, WHITE);
+}
static cstr
-panel_model_picker(Rect bounds, bool input) {
+panel_asset_picker(Rect bounds, bool input) {
if (input && IsKeyPressed(KEY_ESCAPE)) {
- model_picker_close();
+ asset_picker_close();
return NULL;
}
- GuiPanel(bounds, "Choose Model");
+ GuiPanel(bounds, "Choose asset");
f32 left_width = floorf((bounds.width - 24) * 0.45f);
@@ -2699,40 +3073,40 @@ panel_model_picker(Rect bounds, bool input) {
bounds.height - 38,
};
- char previous[sizeof(model_picker.query)];
- memcpy(previous, model_picker.query, sizeof(previous));
+ char previous[sizeof(asset_picker.query)];
+ memcpy(previous, asset_picker.query, sizeof(previous));
/* Ignore the toggle result: the search always keeps text focus. */
- GuiTextBox(search, model_picker.query,
- sizeof(model_picker.query), input);
+ GuiTextBox(search, asset_picker.query,
+ sizeof(asset_picker.query), input);
- if (strcmp(previous, model_picker.query) != 0) {
- model_picker.match_cnt = 0;
+ if (strcmp(previous, asset_picker.query) != 0) {
+ asset_picker.match_cnt = 0;
- for (int i = 0; i < model_picker.item_cnt; ++i) {
- ModelChoice *item = &model_picker.items[i];
+ for (int i = 0; i < asset_picker.item_cnt; ++i) {
+ AssetChoice *item = &asset_picker.items[i];
item->score =
- fuzzy_match(model_picker.query,
+ fuzzy_match(asset_picker.query,
GetFileName(item->key));
if (item->score != INT32_MIN) {
- model_picker.match_cnt++;
+ asset_picker.match_cnt++;
}
}
- if (model_picker.item_cnt > 1) {
- qsort(model_picker.items,
- model_picker.item_cnt,
- sizeof(*model_picker.items),
+ if (asset_picker.item_cnt > 1) {
+ qsort(asset_picker.items,
+ asset_picker.item_cnt,
+ sizeof(*asset_picker.items),
model_choice_compare);
}
- model_picker.sel_idx = 0;
- model_picker.scroll = 0;
+ asset_picker.sel_idx = 0;
+ asset_picker.scroll = 0;
}
- if (input && model_picker.match_cnt) {
+ if (input && asset_picker.match_cnt) {
int step = 0;
if (IsKeyPressed(KEY_UP)) {
@@ -2745,29 +3119,29 @@ panel_model_picker(Rect bounds, bool input) {
step = backwards ? -1 : 1;
}
- model_picker.sel_idx =
- (model_picker.sel_idx + step +
- model_picker.match_cnt) %
- model_picker.match_cnt;
+ asset_picker.sel_idx =
+ (asset_picker.sel_idx + step +
+ asset_picker.match_cnt) %
+ asset_picker.match_cnt;
}
int visible = MAX(1, (int)(list.height / 24));
- if (model_picker.sel_idx < model_picker.scroll) {
- model_picker.scroll = model_picker.sel_idx;
+ if (asset_picker.sel_idx < asset_picker.scroll) {
+ asset_picker.scroll = asset_picker.sel_idx;
}
- if (model_picker.sel_idx >= model_picker.scroll + visible) {
- model_picker.scroll = model_picker.sel_idx - visible + 1;
+ if (asset_picker.sel_idx >= asset_picker.scroll + visible) {
+ asset_picker.scroll = asset_picker.sel_idx - visible + 1;
}
BeginScissorMode((int)list.x, (int)list.y,
(int)list.width, (int)list.height);
for (int row = 0; row < visible; ++row) {
- int index = model_picker.scroll + row;
+ int index = asset_picker.scroll + row;
- if (index >= model_picker.match_cnt) {
+ if (index >= asset_picker.match_cnt) {
break;
}
@@ -2776,34 +3150,34 @@ panel_model_picker(Rect bounds, bool input) {
list.width, 24,
};
- if (index == model_picker.sel_idx) {
+ if (index == asset_picker.sel_idx) {
DrawRectangleRec(item_box,
GetColor(GuiGetStyle(LISTVIEW,
BASE_COLOR_PRESSED)));
}
- GuiLabel(item_box, GetFileName(model_picker.items[index].key));
+ GuiLabel(item_box, GetFileName(asset_picker.items[index].key));
}
- if (!model_picker.match_cnt) {
+ if (!asset_picker.match_cnt) {
GuiLabel((Rect) {
list.x, list.y, list.width, 24
- }, "No matching models");
+ }, "No matching assets");
}
EndScissorMode();
GuiPanel(preview, NULL);
- if (model_picker.match_cnt) {
- ModelChoice *item = &model_picker.items[model_picker.sel_idx];
+ if (asset_picker.match_cnt) {
+ AssetChoice *item = &asset_picker.items[asset_picker.sel_idx];
if (!item->tried_loading) {
item->asset = asset_get(item->key);
item->tried_loading = true;
}
- /* model preview */
+ /* preview */
if (item->asset) {
Rect image = {
preview.x + 1,
@@ -2811,7 +3185,20 @@ panel_model_picker(Rect bounds, bool input) {
preview.width - 2,
preview.height - 35,
};
- model_picker_preview(image, item->asset->mdl);
+ switch (asset_picker.tag) {
+ case ASSET_TAG_MODEL:
+ model_picker_preview(image, item->asset->mdl);
+ break;
+
+ case ASSET_TAG_TEXTURE:
+ texture_picker_preview(image,
+ item->asset->tex);
+ break;
+
+ default:
+ GuiLabel(image, "No preview available");
+ break;
+ }
}
GuiLabel((Rect){
@@ -2819,7 +3206,7 @@ panel_model_picker(Rect bounds, bool input) {
preview.width - 16, 24,
}, item->asset ?
GetFileName(item->key) :
- "Could not load model");
+ "Could not load asset");
if (input && item->asset && IsKeyPressed(KEY_ENTER)) {
return item->key;
@@ -3730,27 +4117,33 @@ panel_draw(EditorPanelContainer container) {
GuiPanel(bounds, "Command Menu");
break;
- case ED_PNL_TYPE_MODEL_PICKER:
- {}
- cstr key = panel_model_picker(bounds, input);
+ case ED_PNL_TYPE_ASSET_PICKER: {
+ cstr key = panel_asset_picker(bounds, input);
if (!key) {
break;
}
- switch (model_picker.mode) {
- case MDL_PICK_COMP_ADD:
- case MDL_PICK_COMP_UPDATE:
- if (editor_set_model(key, model_picker.mode)) {
- model_picker_close();
+ switch (asset_picker.mode) {
+ case ASSET_PICK_MDL_COMP_ADD:
+ case ASSET_PICK_MDL_COMP_UPDATE:
+ if (editor_set_model(key, asset_picker.mode)) {
+ asset_picker_close();
}
break;
- case MDL_PICK_ENT_CREATE:
+ case ASSET_PICK_MDL_ENT_CREATE:
/* TODO: */
break;
+
+ case ASSET_PICK_MAT_TEX:
+ editor_set_material(key, asset_picker.mat_idx,
+ MAT_PROP_TEXTURE);
+ asset_picker_close();
+ break;
}
break;
+ }
case ED_PNL_TYPE_PHYSICS_ELEMENTS:
panel_physics_elements(container);
@@ -3793,6 +4186,31 @@ editor_draw_ui(void) {
panel_draw(ED_PNL_LEFT);
panel_draw(ED_PNL_RIGHT);
+ if (ed.mode == ED_MODE_SCENE &&
+ !ed.panel[ED_PNL_RIGHT].enabled) {
+ char stats[4][64];
+
+ snprintf(stats[0], sizeof(stats[0]), "%i FPS | %.2f ms",
+ GetFPS(), app.frame_time_ms);
+ snprintf(stats[1], sizeof(stats[1]), "Entities: %u",
+ entity_count());
+ snprintf(stats[2], sizeof(stats[2]), "Selected: %i",
+ ed.sel_cnt);
+ snprintf(stats[3], sizeof(stats[3]), "Colliders: %s [C]",
+ ed.draw_physics ? "on" : "off");
+
+ for (int i = 0; i < 4; ++i) {
+ f32 width = editor_label_w(stats[i]);
+
+ GuiLabel((Rect){
+ screen_w - margin - width,
+ margin + i * 22.0f,
+ width,
+ 22.0f
+ }, stats[i]);
+ }
+ }
+
if (ed.interact == ED_INTERACT_SELECT) {
if (started && sel_dist > 20) {
sel_rec_draw();
diff --git a/editor.h b/editor.h
index a63ccbf..d3b3640 100644
--- a/editor.h
+++ b/editor.h
@@ -9,6 +9,7 @@ typedef enum {
ED_ACTION_TRANSFORM,
ED_ACTION_CREATE,
ED_ACTION_DELETE,
+ ED_ACTION_DUPLICATE,
ED_ACTION_RENAME,
ED_ACTION_MODEL,
ED_ACTION_PHYSICS,
@@ -55,7 +56,7 @@ typedef enum {
ED_PNL_TYPE_SCENE,
ED_PNL_TYPE_PROPERTIES,
ED_PNL_TYPE_SPACE,
- ED_PNL_TYPE_MODEL_PICKER,
+ ED_PNL_TYPE_ASSET_PICKER,
ED_PNL_TYPE_PHYSICS_ELEMENTS,
ED_PNL_TYPE_PHYSICS_PROPERTIES,
ED_PNL_TYPE_CMD,
diff --git a/ent.c b/ent.c
index 2b23f0f..055239e 100644
--- a/ent.c
+++ b/ent.c
@@ -250,22 +250,68 @@ entity_system_clear(void) {
static void
entity_draw_mdl(u64 id) {
Entity *ent = entity_get(id);
- if (!ent->mdl.mdl) {
- Asset *asset = asset_get(ent->mdl.key);
+ ModelCmp *model = &ent->mdl;
+
+ if (!model->mdl) {
+ Asset *asset = asset_get(model->key);
if (!asset) {
return;
}
- ent->mdl.mdl = &asset->mdl;
+ model->mdl = &asset->mdl;
}
- Model mdl = *ent->mdl.mdl;
+ Model mdl = *model->mdl;
mdl.transform = entity_world_matrix(id);
Color tint = ent->ed.selected && ed.mode == ED_MODE_SCENE ?
- (Color){ 100, 100, 100, 255 } :
- BLACK;
+ (Color){100, 100, 100, 255} : BLACK;
+
+ u32 count = MIN(model->mat_cnt, (u32)mdl.materialCount);
+
+ if (!count) {
+ DrawModel(mdl, (Vec3){0}, 1, tint);
+ return;
+ }
+
+ struct {
+ MaterialMap albedo;
+ Shader shader;
+ } saved[count];
+
+ for (u32 i = 0; i < count; ++i) {
+ Material *material = &mdl.materials[i];
+ MaterialMap *albedo;
+ albedo = &material->maps[MATERIAL_MAP_ALBEDO];
+
+ saved[i].albedo = *albedo;
+ saved[i].shader = material->shader;
+
+ MaterialOverride *override = &model->materials[i];
+
+ if (override->tex_key) {
+ Asset *asset = asset_get(override->tex_key);
+ if (asset) {
+ albedo->texture = asset->tex;
+ }
+ }
+
+ if (override->shader_key) {
+ FunShader *shader;
+ shader = shader_get(override->shader_key);
+
+ if (shader) {
+ material->shader = shader->sdr;
+ }
+ }
+ }
- DrawModel(mdl, (Vec3) {0}, 1, tint);
+ DrawModel(mdl, (Vec3){0}, 1, tint);
+
+ for (u32 i = 0; i < count; ++i) {
+ Material *material = &mdl.materials[i];
+ material->maps[MATERIAL_MAP_ALBEDO] = saved[i].albedo;
+ material->shader = saved[i].shader;
+ }
}
void
@@ -388,6 +434,46 @@ entity_delete(u64 id) {
en.delete_queue_cnt += 1;
}
+ModelCmp
+entity_model_copy(const ModelCmp *src) {
+ ModelCmp copy = *src;
+ copy.key = src->key ? strdup(src->key) : NULL;
+ copy.materials = NULL;
+
+ if (src->mat_cnt) {
+ copy.materials = calloc(src->mat_cnt,
+ sizeof(*copy.materials));
+ if (!copy.materials) {
+ perror("entity_model_copy: alloc failed.");
+ exit(1);
+ }
+
+ for (u32 i = 0; i < src->mat_cnt; ++i) {
+ const MaterialOverride *from = &src->materials[i];
+ MaterialOverride *to = &copy.materials[i];
+
+ to->tex_key = from->tex_key ?
+ strdup(from->tex_key) : NULL;
+ to->shader_key = from->shader_key ?
+ strdup(from->shader_key) : NULL;
+ }
+ }
+
+ return copy;
+}
+
+void
+entity_model_free(ModelCmp *model) {
+ for (u32 i = 0; i < model->mat_cnt; ++i) {
+ free(model->materials[i].tex_key);
+ free(model->materials[i].shader_key);
+ }
+
+ free(model->materials);
+ free(model->key);
+ *model = (ModelCmp){0};
+}
+
static void *
physics_copy_data(const void *src, size_t size) {
if (!size) {
@@ -396,7 +482,7 @@ physics_copy_data(const void *src, size_t size) {
void *copy = malloc(size);
if (!copy) {
- perror("physics_copy_data");
+ perror("physics_copy_data: alloc failed");
exit(1);
}
@@ -872,7 +958,7 @@ entity_clean(Entity *ent) {
free(ent->name);
/* model component data */
- free(ent->mdl.key);
+ entity_model_free(&ent->mdl);
/* physics component data */
entity_physics_free(&ent->phys);
diff --git a/ent.h b/ent.h
index 81ac395..0b6a1bc 100644
--- a/ent.h
+++ b/ent.h
@@ -6,14 +6,6 @@
#define ENTITY_INVALID_IDX UINT32_MAX
-// typedef enum {
-// ENT_COMP_TRANSFORM,
-// ENT_COMP_MODEL,
-// ENT_COMP_PHYSICS,
-// ENT_COMP_EDITOR,
-// ENT_COMP_CNT,
-// } EntityComponentType;
-
typedef struct {
u64 parent;
u64 first_child;
@@ -23,9 +15,21 @@ typedef struct {
Quaternion rot;
} TransformCmp;
+typedef enum {
+ MAT_PROP_TEXTURE,
+ MAT_PROP_SHADER,
+} MaterialProperty;
+
+typedef struct {
+ char *tex_key;
+ char *shader_key;
+} MaterialOverride;
+
typedef struct {
char *key;
Model *mdl;
+ MaterialOverride *materials;
+ u32 mat_cnt;
bool has, draw;
} ModelCmp;
@@ -97,6 +101,10 @@ Quaternion entity_world_rot(u64 id);
void entity_set_world_pos(Entity*, Vec3);
void entity_set_world_rot(Entity*, Quaternion);
+/* model component data */
+ModelCmp entity_model_copy(const ModelCmp*);
+void entity_model_free(ModelCmp*);
+
/* physics component data */
PhysicsCmp entity_physics_copy(const PhysicsCmp*);
void entity_physics_build(u64 id);
diff --git a/env.c b/env.c
index 2ed7012..412dbea 100644
--- a/env.c
+++ b/env.c
@@ -173,65 +173,71 @@ sky_update_lighting(void) {
}
static void
-env_update_mdl_shader(void) {
+env_update_shader(cstr key) {
Vec3 sun_col = color_to_vec3(env.sun_col);
Vec3 moon_col = color_to_vec3(env.moon_col);
Vec3 ambient_col = color_to_vec3(env.ambient_col);
- Shader sdr = shader_get("model")->sdr;
+ FunShader *res = shader_get(key);
+ if (!res) {
+ TraceLog(LOG_ERROR, "env_update_shaders: invalid key");
+ return;
+ }
+
+ Shader sdr = shader_get(key)->sdr;
SetTraceLogLevel(LOG_DEBUG);
SetShaderValue(
sdr,
- fun_shader_uniform("model", "sun_direction"),
+ fun_shader_uniform(key, "sun_direction"),
&env.sun_dir,
SHADER_UNIFORM_VEC3
);
SetShaderValue(
sdr,
- fun_shader_uniform("model", "sun_colour"),
+ fun_shader_uniform(key, "sun_colour"),
&sun_col,
SHADER_UNIFORM_VEC3
);
SetShaderValue(
sdr,
- fun_shader_uniform("model", "sun_intensity"),
+ fun_shader_uniform(key, "sun_intensity"),
&env.sun_intensity,
SHADER_UNIFORM_FLOAT
);
SetShaderValue(
sdr,
- fun_shader_uniform("model", "moon_direction"),
+ fun_shader_uniform(key, "moon_direction"),
&env.moon_dir,
SHADER_UNIFORM_VEC3
);
SetShaderValue(
sdr,
- fun_shader_uniform("model", "moon_colour"),
+ fun_shader_uniform(key, "moon_colour"),
&moon_col,
SHADER_UNIFORM_VEC3
);
SetShaderValue(
sdr,
- fun_shader_uniform("model", "moon_intensity"),
+ fun_shader_uniform(key, "moon_intensity"),
&env.moon_intensity,
SHADER_UNIFORM_FLOAT
);
SetShaderValue(
sdr,
- fun_shader_uniform("model", "ambient_colour"),
+ fun_shader_uniform(key, "ambient_colour"),
&ambient_col,
SHADER_UNIFORM_VEC3
);
SetShaderValue(
sdr,
- fun_shader_uniform("model", "ambient_intensity"),
+ fun_shader_uniform(key, "ambient_intensity"),
&env.ambient_intensity,
SHADER_UNIFORM_FLOAT
);
@@ -265,11 +271,12 @@ env_update(void) {
// env.cycle += GetFrameTime() / env.day_len;
// env.cycle -= floorf(env.cycle);
// }
+
sky_update_lighting();
- env_update_mdl_shader();
+ env_update_shader("model");
+ env_update_shader("model_cutoff");
}
-
void
env_draw_sky(Camera cam) {
int scr_w = GetScreenWidth();
diff --git a/res.c b/res.c
index ef4f2c2..f48ef86 100644
--- a/res.c
+++ b/res.c
@@ -8,6 +8,21 @@ typedef struct {
static Resources res = {0};
+static void
+asset_set_tag(Asset *asset, cstr path) {
+ cstr ext = GetFileExtension(path);
+ if (strcmp(ext, ".glb") == 0) {
+ asset->tag = ASSET_TAG_MODEL;
+ } else if (strcmp(ext, ".png") == 0) {
+ asset->tag = ASSET_TAG_TEXTURE;
+ } else if (strcmp(ext, ".ttf") == 0 ||
+ strcmp(ext, ".otf") == 0) {
+ asset->tag = ASSET_TAG_FONT;
+ } else if (strcmp(ext, ".ogg") == 0) {
+ asset->tag = ASSET_TAG_SOUND;
+ }
+}
+
static bool
asset_load_data(cstr key) {
Asset *asset = map_get(&res.assets, key);
@@ -122,6 +137,7 @@ asset_unload(cstr key) {
}
}
+
void
res_init(void) {
map_init(&res.sdr);
@@ -131,6 +147,10 @@ res_init(void) {
"res/sdr/mdl_vtx_lit.vs",
"res/sdr/mdl_vtx_lit.fs");
+ fun_shader_create("model_cutoff",
+ "res/sdr/mdl_vtx_lit.vs",
+ "res/sdr/mdl_vtx_lit_cutoff.fs");
+
fun_shader_create("sky", NULL, "res/sdr/sky.fs");
cstr filter = ".glb;.png;.ttf;.otf;.ogg";
@@ -139,6 +159,7 @@ res_init(void) {
for (int i = 0; i < res_list.count; ++i) {
cstr path = res_list.paths[i];
Asset asset = {0};
+ asset_set_tag(&asset, path);
asset.path_strlen = strlen(path);
asset.path = strdup(path); // freed in asset_unload
map_set(&res.assets, asset.path, asset);
@@ -175,7 +196,7 @@ res_update(void) {
}
char**
-asset_mdl_keys(int *count) {
+asset_type_keys(AssetTag tag, int *count) {
*count = 0;
if (!res.assets.base.nnodes) {
@@ -184,7 +205,7 @@ asset_mdl_keys(int *count) {
char **keys = malloc(res.assets.base.nnodes * sizeof(*keys));
if (!keys) {
- perror("asset_mdl_keys: mem allocation failed.");
+ perror("asset_type_keys: mem allocation failed.");
exit(1);
}
@@ -192,11 +213,10 @@ asset_mdl_keys(int *count) {
cstr key;
while ((key = map_next(&res.assets, &iter))) {
- if (strcmp(GetFileExtension(key), ".glb") != 0) {
- continue;
+ Asset *asset = map_get(&res.assets, key);
+ if (asset->tag == tag) {
+ keys[(*count)++] = asset->path;
}
-
- keys[(*count)++] = map_get(&res.assets, key)->path;
}
return keys;
diff --git a/res.h b/res.h
index 7fd2946..a88783a 100644
--- a/res.h
+++ b/res.h
@@ -33,12 +33,11 @@ void res_init(void);
void res_exit(void);
void res_update(void);
-char** asset_mdl_keys(int *count);
+char** asset_type_keys(AssetTag tag, int *count);
Asset* asset_get(cstr key);
FunShader* shader_get(cstr key);
-
void fun_shader_create(cstr key, cstr vpath, cstr fpath);
void fun_shader_delete(FunShader*);
int fun_shader_uniform(cstr skey, cstr ukey);
diff --git a/save.c b/save.c
index db906a2..23f2c1e 100644
--- a/save.c
+++ b/save.c
@@ -516,6 +516,145 @@ write_model_chunk(FILE *fp) {
}
static bool
+read_material_chunk(FILE *fp, SaveLoadContext *ctx, MaterialProperty prop) {
+ u32 count;
+ if (!read_u32_le(fp, &count)) {
+ return false;
+ }
+
+ for (u32 i = 0; i < count; ++i) {
+ u64 pid;
+ u32 mat_idx, key_len;
+
+ if (!read_u64_le(fp, &pid) ||
+ !read_u32_le(fp, &mat_idx) ||
+ !read_u32_le(fp, &key_len)) {
+ return false;
+ }
+
+ if (!pid || pid >= ctx->next_pid ||
+ mat_idx == UINT32_MAX || !key_len) {
+ return false;
+ }
+
+ Entity *ent = entity_get(entity_id_from_pid(pid));
+ if (!ent || !ent->mdl.has) {
+ return false;
+ }
+
+ ModelCmp *model = &ent->mdl;
+
+ if (mat_idx >= model->mat_cnt) {
+ size_t slots = (size_t)mat_idx + 1;
+ if (slots > SIZE_MAX / sizeof(MaterialOverride)) {
+ return false;
+ }
+
+ MaterialOverride *materials;
+ materials = realloc(model->materials,
+ slots * sizeof(*materials));
+
+ if (!materials) {
+ return false;
+ }
+
+ memset(materials + model->mat_cnt, 0,
+ (slots - model->mat_cnt) *
+ sizeof(*materials));
+
+ model->materials = materials;
+ model->mat_cnt = slots;
+ }
+
+ MaterialOverride *material;
+ material = &model->materials[mat_idx];
+
+ char **field = prop == MAT_PROP_TEXTURE ?
+ &material->tex_key : &material->shader_key;
+
+ if (*field) {
+ return false;
+ }
+
+ char *key = malloc((size_t)key_len + 1);
+ if (!key) {
+ return false;
+ }
+
+ if (fread(key, 1, key_len, fp) != key_len) {
+ free(key);
+ return false;
+ }
+
+ key[key_len] = '\0';
+ *field = key;
+ }
+
+ return true;
+}
+
+static bool
+write_material_chunk(FILE *fp, MaterialProperty prop) {
+ SaveChunk chunk = {0};
+ u32 count = 0;
+ cstr label = prop == MAT_PROP_TEXTURE ? "MTEX" : "MSHD";
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ Entity *ent = &entity_data()[i];
+
+ if (!ent->pid || !ent->mdl.has) {
+ continue;
+ }
+
+ for (u32 j = 0; j < ent->mdl.mat_cnt; ++j) {
+ const MaterialOverride *material;
+ material = &ent->mdl.materials[j];
+
+ cstr key = prop == MAT_PROP_TEXTURE ?
+ material->tex_key : material->shader_key;
+
+ count += key != NULL;
+ }
+ }
+
+ if (!chunk_begin(fp, label, &chunk) ||
+ !write_u32_le(fp, count)) {
+ return false;
+ }
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ Entity *ent = &entity_data()[i];
+
+ if (!ent->pid || !ent->mdl.has) {
+ continue;
+ }
+
+ for (u32 j = 0; j < ent->mdl.mat_cnt; ++j) {
+ const MaterialOverride *material;
+ material = &ent->mdl.materials[j];
+
+ cstr key = prop == MAT_PROP_TEXTURE ?
+ material->tex_key : material->shader_key;
+
+ if (!key) {
+ continue;
+ }
+
+ u32 key_len = (u32)strlen(key);
+
+ if (!write_u64_le(fp, ent->pid) ||
+ !write_u32_le(fp, j) ||
+ !write_u32_le(fp, key_len) ||
+ !write_byte_arr(fp, key, key_len)) {
+ return false;
+ }
+ }
+ }
+
+ return chunk_end(fp, &chunk);
+}
+
+static bool
read_physics_shape(FILE *fp, PhysicsShape *shape) {
*shape = (PhysicsShape){0};
@@ -1006,6 +1145,18 @@ save_file_read(cstr path) {
break;
}
success = read_model_chunk(fp, &ctx);
+ } else if (memcmp(label, "MTEX", 4) ==0 ||
+ memcmp(label, "MSHD", 4) == 0) {
+ if (!read_entt) {
+ success = false;
+ break;
+ }
+
+ MaterialProperty prop;
+ prop = memcmp(label, "MTEX", 4) == 0 ?
+ MAT_PROP_TEXTURE : MAT_PROP_SHADER;
+
+ success = read_material_chunk(fp, &ctx, prop);
} else if (memcmp(label, "PHYS", 4) == 0) {
if (!read_entt) {
TraceLog(LOG_ERROR, "PHYS found before ENTT.");
@@ -1059,6 +1210,8 @@ save_file_write(cstr path) {
write_entity_chunk(fp) &&
write_transform_chunk(fp) &&
write_model_chunk(fp) &&
+ write_material_chunk(fp, MAT_PROP_TEXTURE) &&
+ write_material_chunk(fp, MAT_PROP_SHADER) &&
write_physics_chunk(fp);
if (fclose(fp) != 0) {