From 186b7651ddae279188fedd91010a6d7017431cae Mon Sep 17 00:00:00 2001 From: bjorn Date: Fri, 31 Jul 2026 11:24:24 -0700 Subject: [PATCH 1/7] Mutable materials; --- CHANGES.md | 1 + etc/shaders/lovr.glsl | 13 +- src/api/api.h | 3 + src/api/l_graphics.c | 206 +++++++------ src/api/l_graphics_material.c | 150 +++++++-- src/api/l_graphics_model.c | 13 +- src/core/spv.c | 2 +- src/modules/graphics/graphics.c | 529 ++++++++++++++++++++------------ src/modules/graphics/graphics.h | 66 ++-- 9 files changed, 620 insertions(+), 363 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3e518cb7bc..9051271da0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,6 +20,7 @@ dev - Add `sleep` callback to `World:setCallbacks`. - Add `linearDamping` and `angularDamping` settings to `lovr.physics.newWorld`. - Add `lovr.headset.getHandPosition/Orientation/Pose`. +- Add `Material:get/setNumber`, `Material:get/setColor`, `Material:get/setTexture`, and `Material:get/setQuad`. ### Change diff --git a/etc/shaders/lovr.glsl b/etc/shaders/lovr.glsl index bd4200f756..485bd505d3 100644 --- a/etc/shaders/lovr.glsl +++ b/etc/shaders/lovr.glsl @@ -38,11 +38,6 @@ layout(set = 0, binding = 2) uniform DrawBuffer { layout(row_major) Draw Draws[2 layout(set = 0, binding = 3) uniform sampler Sampler; struct MaterialData { - vec4 color; - vec4 glow; - vec2 uvShift; - vec2 uvScale; - vec2 sdfRange; float metalness; float roughness; float clearcoat; @@ -50,6 +45,10 @@ struct MaterialData { float occlusionStrength; float normalScale; float alphaCutoff; + vec4 color; + vec4 glow; + vec4 quad; + vec2 sdfRange; }; layout(set = 1, binding = 0) uniform MaterialBuffer { @@ -558,8 +557,8 @@ void main() { #endif if (flag_uvTransform) { - UV *= Material.uvScale; - UV += Material.uvShift; + UV *= Material.quad.zw; + UV += Material.quad.xy; } } #endif diff --git a/src/api/api.h b/src/api/api.h index 92b123d0d2..3a355cc8a6 100644 --- a/src/api/api.h +++ b/src/api/api.h @@ -51,6 +51,9 @@ extern StringEntry lovrHorizontalAlign[]; extern StringEntry lovrJointType[]; extern StringEntry lovrKeyboardKey[]; extern StringEntry lovrLayerType[]; +extern StringEntry lovrMaterialColor[]; +extern StringEntry lovrMaterialNumber[]; +extern StringEntry lovrMaterialTexture[]; extern StringEntry lovrMeshStorage[]; extern StringEntry lovrModelDrawMode[]; extern StringEntry lovrMotorMode[]; diff --git a/src/api/l_graphics.c b/src/api/l_graphics.c index 698241ab95..60594103cb 100644 --- a/src/api/l_graphics.c +++ b/src/api/l_graphics.c @@ -163,6 +163,34 @@ StringEntry lovrHorizontalAlign[] = { { 0 } }; +StringEntry lovrMaterialColor[] = { + [COLOR_BASE] = ENTRY("base"), + [COLOR_GLOW] = ENTRY("glow"), + { 0 } +}; + +StringEntry lovrMaterialNumber[] = { + [NUMBER_METALNESS] = ENTRY("metalness"), + [NUMBER_ROUGHNESS] = ENTRY("roughness"), + [NUMBER_CLEARCOAT] = ENTRY("clearcoat"), + [NUMBER_CLEARCOAT_ROUGHNESS] = ENTRY("clearcoatroughness"), + [NUMBER_OCCLUSION_STRENGTH] = ENTRY("occlusionstrength"), + [NUMBER_NORMAL_SCALE] = ENTRY("normalscale"), + [NUMBER_ALPHA_CUTOFF] = ENTRY("alphacutoff"), + { 0 } +}; + +StringEntry lovrMaterialTexture[] = { + [TEXTURE_COLOR] = ENTRY("color"), + [TEXTURE_GLOW] = ENTRY("glow"), + [TEXTURE_METALNESS] = ENTRY("metalness"), + [TEXTURE_ROUGHNESS] = ENTRY("roughness"), + [TEXTURE_CLEARCOAT] = ENTRY("clearcoat"), + [TEXTURE_OCCLUSION] = ENTRY("occlusion"), + [TEXTURE_NORMAL] = ENTRY("normal"), + { 0 } +}; + StringEntry lovrMeshStorage[] = { [MESH_CPU] = ENTRY("cpu"), [MESH_GPU] = ENTRY("gpu"), @@ -1459,112 +1487,98 @@ static Texture* luax_opttexture(lua_State* L, int index) { } static int l_lovrGraphicsNewMaterial(lua_State* L) { - MaterialInfo info; - memset(&info, 0, sizeof(info)); - - luaL_checktype(L, 1, LUA_TTABLE); - - lua_getfield(L, 1, "color"); - luax_optcolor(L, -1, info.data.color); - lua_pop(L, 1); - - lua_getfield(L, 1, "glow"); - if (lua_isnil(L, -1)) { - memset(info.data.glow, 0, sizeof(info.data.glow)); - } else { - luax_optcolor(L, -1, info.data.glow); - } - lua_pop(L, 1); - - lua_getfield(L, 1, "uvShift"); - if (lua_type(L, -1) == LUA_TNUMBER) { - float shift = lua_tonumber(L, -1); - info.data.uvShift[0] = shift; - info.data.uvShift[1] = shift; - } else if (lua_type(L, -1) == LUA_TTABLE) { - lua_rawgeti(L, -1, 1); - lua_rawgeti(L, -2, 2); - info.data.uvShift[0] = luax_optfloat(L, -2, 0.f); - info.data.uvShift[1] = luax_optfloat(L, -1, 0.f); - lua_pop(L, 2); - } - lua_pop(L, 1); - - lua_getfield(L, 1, "uvScale"); - if (lua_isnil(L, -1)) { - info.data.uvScale[0] = 1.f; - info.data.uvScale[1] = 1.f; - } else if (lua_isnumber(L, -1)) { - float scale = lua_tonumber(L, -1); - info.data.uvScale[0] = scale; - info.data.uvScale[1] = scale; - } else if (lua_type(L, -1) == LUA_TTABLE) { - lua_rawgeti(L, -1, 1); - lua_rawgeti(L, -2, 2); - info.data.uvScale[0] = luax_optfloat(L, -2, 1.f); - info.data.uvScale[1] = luax_optfloat(L, -1, 1.f); - lua_pop(L, 2); - } - lua_pop(L, 1); - - lua_getfield(L, 1, "metalness"); - info.data.metalness = luax_optfloat(L, -1, 1.f); - lua_pop(L, 1); - - lua_getfield(L, 1, "roughness"); - info.data.roughness = luax_optfloat(L, -1, 1.f); - lua_pop(L, 1); - - lua_getfield(L, 1, "clearcoat"); - info.data.clearcoat = luax_optfloat(L, -1, 0.f); - lua_pop(L, 1); - - lua_getfield(L, 1, "clearcoatRoughness"); - info.data.clearcoatRoughness = luax_optfloat(L, -1, 0.f); - lua_pop(L, 1); - - lua_getfield(L, 1, "occlusionStrength"); - info.data.occlusionStrength = luax_optfloat(L, -1, 1.f); - lua_pop(L, 1); + Material* material = lovrMaterialCreate(luax_totype(L, 1, Texture)); + luax_assert(L, material); - lua_getfield(L, 1, "normalScale"); - info.data.normalScale = luax_optfloat(L, -1, 1.f); - lua_pop(L, 1); + if (lua_istable(L, 1)) { + float value, color[4]; - lua_getfield(L, 1, "alphaCutoff"); - info.data.alphaCutoff = luax_optfloat(L, -1, 0.f); - lua_pop(L, 1); + for (uint32_t i = 0; i < NUMBER_COUNT; i++) { + lua_pushlstring(L, lovrMaterialNumber[i].string, lovrMaterialNumber[i].length); + lua_gettable(L, 1); + if (!lua_isnil(L, -1)) lovrMaterialSetNumber(material, i, luax_checkfloat(L, -1)); + lua_pop(L, 1); + } - lua_getfield(L, 1, "texture"); - info.texture = luax_opttexture(L, -1); - lua_pop(L, 1); + for (uint32_t i = 0; i < COLOR_COUNT; i++) { + if (i == 0) { + lua_pushliteral(L, "color"); + } else { + lua_pushlstring(L, lovrMaterialColor[i].string, lovrMaterialColor[i].length); + } - lua_getfield(L, 1, "glowTexture"); - info.glowTexture = luax_opttexture(L, -1); - lua_pop(L, 1); + lua_gettable(L, 1); + if (!lua_isnil(L, -1)) { + luax_optcolor(L, -1, color); + lovrMaterialSetColor(material, i, color); + } + lua_pop(L, 1); + } - lua_getfield(L, 1, "metalnessTexture"); - info.metalnessTexture = luax_opttexture(L, -1); - lua_pop(L, 1); + for (uint32_t i = 0; i < TEXTURE_COUNT; i++) { + if (i == 0) { + lua_pushliteral(L, "texture"); + } else { + char key[64]; + size_t length = lovrMaterialTexture[i].length; + memcpy(key, lovrMaterialTexture[i].string, length); + memcpy(key + length, "Texture", strlen("Texture")); + lua_pushlstring(L, key, length + strlen("Texture")); + } + lua_gettable(L, 1); + if (!lua_isnil(L, -1)) { + Texture* texture = luax_checktype(L, -1, Texture); + if (!lovrMaterialSetTexture(material, i, texture)) { + lovrRelease(material, lovrMaterialDestroy); + luax_throw(L); + } + } + lua_pop(L, 1); + } - lua_getfield(L, 1, "roughnessTexture"); - info.roughnessTexture = luax_opttexture(L, -1); - lua_pop(L, 1); + float quad[4]; - lua_getfield(L, 1, "clearcoatTexture"); - info.clearcoatTexture = luax_opttexture(L, -1); - lua_pop(L, 1); + // Deprecated + lua_getfield(L, 1, "uvShift"); + if (lua_istable(L, -1)) { + lua_rawgeti(L, -1, 1); + lua_rawgeti(L, -2, 2); + quad[0] = luax_optfloat(L, -2, 0.f); + quad[1] = luax_optfloat(L, -1, 0.f); + lua_pop(L, 2); + } else if (lua_type(L, -1) == LUA_TNUMBER) { + quad[0] = quad[1] = lua_tonumber(L, -1); + } + lua_pop(L, 1); - lua_getfield(L, 1, "occlusionTexture"); - info.occlusionTexture = luax_opttexture(L, -1); - lua_pop(L, 1); + // Deprecated + lua_getfield(L, 1, "uvScale"); + if (lua_istable(L, -1)) { + lua_rawgeti(L, -1, 1); + lua_rawgeti(L, -2, 2); + quad[2] = luax_optfloat(L, -2, 1.f); + quad[3] = luax_optfloat(L, -1, 1.f); + lua_pop(L, 2); + } else if (lua_type(L, -1) == LUA_TNUMBER) { + quad[2] = quad[3] = lua_tonumber(L, -1); + } + lua_pop(L, 1); - lua_getfield(L, 1, "normalTexture"); - info.normalTexture = luax_opttexture(L, -1); - lua_pop(L, 1); + lua_getfield(L, 1, "quad"); + if (lua_istable(L, -1)) { + lua_rawgeti(L, -1, 1); + lua_rawgeti(L, -2, 2); + lua_rawgeti(L, -3, 3); + lua_rawgeti(L, -4, 4); + quad[0] = luax_optfloat(L, -4, 0.f); + quad[1] = luax_optfloat(L, -3, 0.f); + quad[2] = luax_optfloat(L, -2, 1.f); + quad[3] = luax_optfloat(L, -1, 1.f); + lua_pop(L, 4); + } + lua_pop(L, 1); + } - Material* material = lovrMaterialCreate(&info); - luax_assert(L, material); luax_pushtype(L, Material, material); lovrRelease(material, lovrMaterialDestroy); return 1; diff --git a/src/api/l_graphics_material.c b/src/api/l_graphics_material.c index a3d2a4d6f1..be0bea126e 100644 --- a/src/api/l_graphics_material.c +++ b/src/api/l_graphics_material.c @@ -17,66 +17,162 @@ Material* luax_optmaterial(lua_State* L, int index) { } } +static int l_lovrMaterialGetNumber(lua_State* L) { + Material* material = luax_checktype(L, 1, Material); + MaterialNumber key = luax_checkenum(L, 2, MaterialNumber, NULL); + float number = lovrMaterialGetNumber(material, key); + lua_pushnumber(L, number); + return 1; +} + +static int l_lovrMaterialSetNumber(lua_State* L) { + Material* material = luax_checktype(L, 1, Material); + MaterialNumber key = luax_checkenum(L, 2, MaterialNumber, NULL); + float number = luax_checkfloat(L, 3); + lovrMaterialSetNumber(material, key, number); + return 0; +} + +static int l_lovrMaterialGetColor(lua_State* L) { + Material* material = luax_checktype(L, 1, Material); + MaterialColor key = luax_checkenum(L, 2, MaterialColor, "base"); + const float* color = lovrMaterialGetColor(material, key); + lua_pushnumber(L, color[0]); + lua_pushnumber(L, color[1]); + lua_pushnumber(L, color[2]); + lua_pushnumber(L, color[3]); + return 4; +} + +static int l_lovrMaterialSetColor(lua_State* L) { + Material* material = luax_checktype(L, 1, Material); + int index = 2; + float color[4]; + MaterialColor key = lua_type(L, index) == LUA_TSTRING ? luax_checkenum(L, index++, MaterialColor, NULL) : COLOR_BASE; + luax_readcolor(L, index, color); + lovrMaterialSetColor(material, key, color); + return 0; +} + +static int l_lovrMaterialGetTexture(lua_State* L) { + Material* material = luax_checktype(L, 1, Material); + MaterialTexture key = luax_checkenum(L, 2, MaterialTexture, "color"); + Texture* texture = lovrMaterialGetTexture(material, key); + luax_pushtype(L, Texture, texture); + return 1; +} + +static int l_lovrMaterialSetTexture(lua_State* L) { + Material* material = luax_checktype(L, 1, Material); + int index = 2; + MaterialTexture key = lua_type(L, index) == LUA_TSTRING ? luax_checkenum(L, index++, MaterialTexture, NULL) : TEXTURE_COLOR; + Texture* texture = luax_checktype(L, index, Texture); + lovrMaterialSetTexture(material, key, texture); + return 0; +} + +static int l_lovrMaterialGetQuad(lua_State* L) { + Material* material = luax_checktype(L, 1, Material); + float ox, oy, sx, sy; + lovrMaterialGetQuad(material, &ox, &oy, &sx, &sy); + lua_pushnumber(L, ox); + lua_pushnumber(L, oy); + lua_pushnumber(L, sx); + lua_pushnumber(L, sy); + return 4; +} + +static int l_lovrMaterialSetQuad(lua_State* L) { + Material* material = luax_checktype(L, 1, Material); + float ox = luax_checkfloat(L, 2); + float oy = luax_checkfloat(L, 3); + float sx = luax_checkfloat(L, 4); + float sy = luax_checkfloat(L, 5); + lovrMaterialSetQuad(material, ox, oy, sx, sy); + return 0; +} + +// Deprecated static int l_lovrMaterialGetProperties(lua_State* L) { Material* material = luax_checktype(L, 1, Material); - const MaterialInfo* info = lovrMaterialGetInfo(material); + lua_newtable(L); + lua_pushnumber(L, lovrMaterialGetNumber(material, NUMBER_METALNESS)), lua_setfield(L, -2, "metalness"); + lua_pushnumber(L, lovrMaterialGetNumber(material, NUMBER_ROUGHNESS)), lua_setfield(L, -2, "roughness"); + lua_pushnumber(L, lovrMaterialGetNumber(material, NUMBER_CLEARCOAT)), lua_setfield(L, -2, "clearcoat"); + lua_pushnumber(L, lovrMaterialGetNumber(material, NUMBER_CLEARCOAT_ROUGHNESS)), lua_setfield(L, -2, "clearcoatRoughness"); + lua_pushnumber(L, lovrMaterialGetNumber(material, NUMBER_OCCLUSION_STRENGTH)), lua_setfield(L, -2, "occlusionStrength"); + lua_pushnumber(L, lovrMaterialGetNumber(material, NUMBER_NORMAL_SCALE)), lua_setfield(L, -2, "normalScale"); + lua_pushnumber(L, lovrMaterialGetNumber(material, NUMBER_ALPHA_CUTOFF)), lua_setfield(L, -2, "alphaCutoff"); + + const float* color; + color = lovrMaterialGetColor(material, COLOR_BASE); + lua_createtable(L, 4, 0); - lua_pushnumber(L, info->data.color[0]); + lua_pushnumber(L, color[0]); lua_rawseti(L, -2, 1); - lua_pushnumber(L, info->data.color[1]); + lua_pushnumber(L, color[1]); lua_rawseti(L, -2, 2); - lua_pushnumber(L, info->data.color[2]); + lua_pushnumber(L, color[2]); lua_rawseti(L, -2, 3); - lua_pushnumber(L, info->data.color[3]); + lua_pushnumber(L, color[3]); lua_rawseti(L, -2, 4); lua_setfield(L, -2, "color"); + color = lovrMaterialGetColor(material, COLOR_GLOW); + lua_createtable(L, 4, 0); - lua_pushnumber(L, info->data.glow[0]); + lua_pushnumber(L, color[0]); lua_rawseti(L, -2, 1); - lua_pushnumber(L, info->data.glow[1]); + lua_pushnumber(L, color[1]); lua_rawseti(L, -2, 2); - lua_pushnumber(L, info->data.glow[2]); + lua_pushnumber(L, color[2]); lua_rawseti(L, -2, 3); - lua_pushnumber(L, info->data.glow[3]); + lua_pushnumber(L, color[3]); lua_rawseti(L, -2, 4); lua_setfield(L, -2, "glow"); + float ox, oy, sx, sy; + lovrMaterialGetQuad(material, &ox, &oy, &sx, &sy); + lua_createtable(L, 2, 0); - lua_pushnumber(L, info->data.uvShift[0]); + lua_pushnumber(L, ox); lua_rawseti(L, -2, 1); - lua_pushnumber(L, info->data.uvShift[1]); + lua_pushnumber(L, oy); lua_rawseti(L, -2, 2); lua_setfield(L, -2, "uvShift"); lua_createtable(L, 2, 0); - lua_pushnumber(L, info->data.uvScale[0]); + lua_pushnumber(L, sx); lua_rawseti(L, -2, 1); - lua_pushnumber(L, info->data.uvScale[1]); + lua_pushnumber(L, sy); lua_rawseti(L, -2, 2); lua_setfield(L, -2, "uvScale"); - lua_pushnumber(L, info->data.metalness), lua_setfield(L, -2, "metalness"); - lua_pushnumber(L, info->data.roughness), lua_setfield(L, -2, "roughness"); - lua_pushnumber(L, info->data.clearcoat), lua_setfield(L, -2, "clearcoat"); - lua_pushnumber(L, info->data.clearcoatRoughness), lua_setfield(L, -2, "clearcoatRoughness"); - lua_pushnumber(L, info->data.occlusionStrength), lua_setfield(L, -2, "occlusionStrength"); - lua_pushnumber(L, info->data.normalScale), lua_setfield(L, -2, "normalScale"); - lua_pushnumber(L, info->data.alphaCutoff), lua_setfield(L, -2, "alphaCutoff"); - luax_pushtype(L, Texture, info->texture), lua_setfield(L, -2, "texture"); - luax_pushtype(L, Texture, info->glowTexture), lua_setfield(L, -2, "glowTexture"); - luax_pushtype(L, Texture, info->metalnessTexture), lua_setfield(L, -2, "metalnessTexture"); - luax_pushtype(L, Texture, info->roughnessTexture), lua_setfield(L, -2, "roughnessTexture"); - luax_pushtype(L, Texture, info->clearcoatTexture), lua_setfield(L, -2, "clearcoatTexture"); - luax_pushtype(L, Texture, info->occlusionTexture), lua_setfield(L, -2, "occlusionTexture"); - luax_pushtype(L, Texture, info->normalTexture), lua_setfield(L, -2, "normalTexture"); + luax_pushtype(L, Texture, lovrMaterialGetTexture(material, TEXTURE_COLOR)), lua_setfield(L, -2, "texture"); + luax_pushtype(L, Texture, lovrMaterialGetTexture(material, TEXTURE_GLOW)), lua_setfield(L, -2, "glowTexture"); + luax_pushtype(L, Texture, lovrMaterialGetTexture(material, TEXTURE_METALNESS)), lua_setfield(L, -2, "metalnessTexture"); + luax_pushtype(L, Texture, lovrMaterialGetTexture(material, TEXTURE_ROUGHNESS)), lua_setfield(L, -2, "roughnessTexture"); + luax_pushtype(L, Texture, lovrMaterialGetTexture(material, TEXTURE_CLEARCOAT)), lua_setfield(L, -2, "clearcoatTexture"); + luax_pushtype(L, Texture, lovrMaterialGetTexture(material, TEXTURE_OCCLUSION)), lua_setfield(L, -2, "occlusionTexture"); + luax_pushtype(L, Texture, lovrMaterialGetTexture(material, TEXTURE_NORMAL)), lua_setfield(L, -2, "normalTexture"); return 1; } const luaL_Reg lovrMaterial[] = { + { "getNumber", l_lovrMaterialGetNumber }, + { "setNumber", l_lovrMaterialSetNumber }, + { "getColor", l_lovrMaterialGetColor }, + { "setColor", l_lovrMaterialSetColor }, + { "getTexture", l_lovrMaterialGetTexture }, + { "setTexture", l_lovrMaterialSetTexture }, + { "getQuad", l_lovrMaterialGetQuad }, + { "setQuad", l_lovrMaterialSetQuad }, + + // Deprecated { "getProperties", l_lovrMaterialGetProperties }, + { NULL, NULL } }; diff --git a/src/api/l_graphics_model.c b/src/api/l_graphics_model.c index 0a118cd726..63e73d3d0f 100644 --- a/src/api/l_graphics_model.c +++ b/src/api/l_graphics_model.c @@ -247,12 +247,20 @@ static int l_lovrModelGetTexture(lua_State* L) { static int l_lovrModelGetMaterial(lua_State* L) { Model* model = luax_checktype(L, 1, Model); uint32_t index = luax_checkmaterialindex(L, 2, lovrModelGetMetadata(model)); - Material* material = lovrModelGetMaterial(model, index); - luax_assert(L, material); + Material* material; + luax_assert(L, lovrModelGetMaterial(model, index, &material)); luax_pushtype(L, Material, material); return 1; } +static int l_lovrModelSetMaterial(lua_State* L) { + Model* model = luax_checktype(L, 1, Model); + uint32_t index = luax_checkmaterialindex(L, 2, lovrModelGetMetadata(model)); + Material* material = luax_optmaterial(L, 3); + luax_assert(L, lovrModelSetMaterial(model, index, material)); + return 0; +} + static int l_lovrModelBuildRaytracer(lua_State* L) { Model* model = luax_checktype(L, 1, Model); luax_assert(L, lovrModelBuildRaytracer(model)); @@ -402,6 +410,7 @@ const luaL_Reg lovrModel[] = { { "getMaterialCount", l_lovrModelMetaGetMaterialCount }, { "getMaterialName", l_lovrModelMetaGetMaterialName }, { "getMaterial", l_lovrModelGetMaterial }, + { "setMaterial", l_lovrModelSetMaterial }, { "buildRaytracer", l_lovrModelBuildRaytracer }, diff --git a/src/core/spv.c b/src/core/spv.c index c8bc55d9b9..9e820bd7dd 100644 --- a/src/core/spv.c +++ b/src/core/spv.c @@ -173,7 +173,7 @@ const char* spv_result_to_string(spv_result result) { case SPV_INVALID: return "Invalid SPIR-V"; case SPV_TOO_BIG: return "SPIR-V contains too many types/variables (max ID is 65534)"; case SPV_UNSUPPORTED_SPEC_CONSTANT_TYPE: return "This type of specialization constant is not supported"; - case SPV_UNSUPPORTED_DATA_TYPE: return "Struct fields must be square float matrices, float/int/uint vectors, 32 bit numbers, or bools"; + case SPV_UNSUPPORTED_DATA_TYPE: return "Struct fields must be square float matrices, float/int/uint vectors, 8, 16, or 32 bit numbers, or bools"; default: return NULL; } } diff --git a/src/modules/graphics/graphics.c b/src/modules/graphics/graphics.c index 899c79caa5..ba7075425b 100644 --- a/src/modules/graphics/graphics.c +++ b/src/modules/graphics/graphics.c @@ -29,10 +29,12 @@ #define MAX_PIPELINES 8192 #define MAX_TALLIES 255 +#define MATERIAL_BLOCK_SIZE 256 #define TRANSFORM_STACK_SIZE 16 #define PIPELINE_STACK_SIZE 8 #define MAX_SHADER_RESOURCES 32 #define MAX_CUSTOM_ATTRIBUTES 10 +#define MATERIAL_STRIDE ALIGN(sizeof(MaterialData), state.limits.uniformBufferAlign) #define FLOAT_BITS(f) ((union { float f; uint32_t u; }) { f }).u typedef struct { @@ -176,26 +178,41 @@ struct Shader { char* names; }; +typedef struct { + float numbers[NUMBER_COUNT]; + float padding; + float colors[COLOR_COUNT][4]; + float quad[4]; + float sdfRange[2]; +} MaterialData; + +typedef struct { + uint32_t next; + uint32_t tick; +} MaterialNode; + typedef struct { void* next; - void* pointer; - Material* materials; + uint32_t head; + uint32_t tail; + MaterialNode nodes[MATERIAL_BLOCK_SIZE]; gpu_buffer* buffer; + void* bufferPointer; gpu_bundle_pool* bundlePool; gpu_bundle* bundles; - uint32_t head; - uint32_t tail; } MaterialBlock; struct Material { atomic_uint ref; - uint32_t next; - uint32_t tick; uint32_t index; - MaterialInfo info; - gpu_bundle* bundle; MaterialBlock* block; - bool hasWritableTexture; + MaterialData data; + Texture* textures[TEXTURE_COUNT]; + gpu_binding bindings[1 + TEXTURE_COUNT]; + gpu_bundle* bundle; + MaterialData* pointer; + uint32_t bundleTick; + uint32_t bufferTick; }; typedef struct { @@ -792,15 +809,7 @@ bool lovrGraphicsInit(GraphicsConfig* config) { // Default Material - state.defaultMaterial = lovrMaterialCreate(&(MaterialInfo) { - .data.color = { 1.f, 1.f, 1.f, 1.f }, - .data.uvScale = { 1.f, 1.f }, - .data.metalness = 0.f, - .data.roughness = 1.f, - .data.normalScale = 1.f, - .texture = state.defaultTexture - }); - + state.defaultMaterial = lovrMaterialCreate(NULL); if (!state.defaultMaterial) goto fail; // Default Samplers @@ -918,7 +927,6 @@ void lovrGraphicsDestroy(void) { MaterialBlock* next = block->next; gpu_bundle_pool_destroy(block->bundlePool); gpu_buffer_destroy(block->buffer); - lovrFree(block->materials); lovrFree(block->bundlePool); lovrFree(block->bundles); lovrFree(block->buffer); @@ -3100,11 +3108,7 @@ void lovrTextureSetSampler(Texture* texture, Sampler* sampler) { Material* lovrTextureToMaterial(Texture* texture) { if (!texture->material) { - texture->material = lovrMaterialCreate(&(MaterialInfo) { - .data.color = { 1.f, 1.f, 1.f, 1.f }, - .data.uvScale = { 1.f, 1.f }, - .texture = texture - }); + texture->material = lovrMaterialCreate(texture); if (!texture->material) { return NULL; @@ -3113,7 +3117,7 @@ Material* lovrTextureToMaterial(Texture* texture) { // Since the Material refcounts the Texture, this creates a cycle. Release the texture to make // sure this is a weak relationship (the automaterial does not keep the texture refcounted). lovrRelease(texture, lovrTextureDestroy); - texture->material->info.texture = NULL; + texture->material->textures[TEXTURE_COLOR] = NULL; } return texture->material; @@ -4116,164 +4120,272 @@ const DataField* lovrShaderGetBufferFormat(Shader* shader, const char* name, uin // Material -Material* lovrMaterialCreate(const MaterialInfo* info) { - Texture* textures[] = { - info->texture, - info->glowTexture, - info->metalnessTexture, - info->roughnessTexture, - info->clearcoatTexture, - info->occlusionTexture, - info->normalTexture - }; - - for (uint32_t i = 0; i < COUNTOF(textures); i++) { - if (!textures[i]) continue; - lovrCheck(i == 0 || textures[i]->info.type == TEXTURE_2D, "Material textures must be 2D"); - lovrCheck(textures[i]->info.samples == 1, "Material textures can not be multisampled"); - lovrCheck(textures[i]->info.usage & TEXTURE_SAMPLE, "Textures must be created with the 'sample' usage to use them in Materials"); - } - - // TODO this should be more fine-grained +static bool lovrMaterialAllocate(Material* material) { mtx_lock(&state.lock); MaterialBlock* block = NULL; - - for (MaterialBlock* node = state.materials; node != NULL; node = node->next) { - if (node->head != ~0u && gpu_is_complete(node->materials[node->head].tick)) { - block = node; + for (MaterialBlock* b = state.materials; b != NULL; b = b->next) { + if (b->head != ~0u && gpu_is_complete(b->nodes[b->head].tick)) { + block = b; break; } } if (!block) { - const uint32_t count = 256; - block = lovrMalloc(sizeof(*block)); - block->materials = lovrMalloc(count * sizeof(Material)); + block = lovrMalloc(sizeof(MaterialBlock)); block->buffer = lovrMalloc(gpu_sizeof_buffer()); block->bundlePool = lovrMalloc(gpu_sizeof_bundle_pool()); - block->bundles = lovrMalloc(count * gpu_sizeof_bundle()); + block->bundles = lovrMalloc(MATERIAL_BLOCK_SIZE * gpu_sizeof_bundle()); - for (uint32_t i = 0; i < count; i++) { - block->materials[i] = (Material) { - .index = i, - .next = i + 1, - .block = block, - .bundle = (gpu_bundle*) ((char*) block->bundles + i * gpu_sizeof_bundle()) - }; + for (uint32_t i = 0; i < MATERIAL_BLOCK_SIZE; i++) { + block->nodes[i].next = i + 1; + block->nodes[i].tick = 0; } - block->materials[count - 1].next = ~0u; - block->tail = count - 1; block->head = 0; + block->tail = MATERIAL_BLOCK_SIZE - 1; + block->nodes[block->tail].next = ~0u; gpu_bundle_pool_info poolInfo = { .bundles = block->bundles, .layout = state.materialLayout->gpu, - .count = count + .count = MATERIAL_BLOCK_SIZE }; if (!gpu_bundle_pool_init(block->bundlePool, &poolInfo)) { - lovrFree(block->materials); lovrFree(block->buffer); lovrFree(block->bundlePool); lovrFree(block->bundles); mtx_unlock(&state.lock); - return NULL; + return false; } gpu_buffer_info bufferInfo = { .type = GPU_BUFFER_STATIC, - .size = count * (uint32_t) ALIGN(sizeof(MaterialData), state.limits.uniformBufferAlign), - .pointer = &block->pointer + .size = MATERIAL_BLOCK_SIZE * MATERIAL_STRIDE, + .pointer = &block->bufferPointer }; if (!gpu_buffer_init(block->buffer, &bufferInfo)) { - lovrFree(block->materials); lovrFree(block->buffer); lovrFree(block->bundlePool); lovrFree(block->bundles); gpu_bundle_pool_destroy(block->bundlePool); mtx_unlock(&state.lock); - return NULL; + return false; } block->next = state.materials; state.materials = block; } - Material* material = &block->materials[block->head]; - material->ref = 1; - material->info = *info; + material->block = block; + material->index = block->head; + material->bundle = (gpu_bundle*) ((char*) block->bundles + block->head * gpu_sizeof_bundle()); + material->bundleTick = state.tick; - MaterialData* data; - uint32_t stride = ALIGN(sizeof(MaterialData), state.limits.uniformBufferAlign); + material->bindings[0] = (gpu_binding) { + .number = 0, + .type = GPU_SLOT_UNIFORM_BUFFER, + .buffer.object = block->buffer, + .buffer.offset = material->index * MATERIAL_STRIDE, + .buffer.extent = MATERIAL_STRIDE + }; - if (block->pointer) { - data = (MaterialData*) ((char*) block->pointer + material->index * stride); - } else { - BufferView staging = getBuffer(GPU_BUFFER_UPLOAD, sizeof(MaterialData), 4); - if (!staging.buffer) return mtx_unlock(&state.lock), NULL; + if (block->bufferPointer) { + material->pointer = (MaterialData*) ((char*) block->bufferPointer + material->index * MATERIAL_STRIDE); + material->bufferTick = state.tick; + } - gpu_copy_buffers(state.stream, staging.buffer, block->buffer, staging.offset, stride * material->index, sizeof(MaterialData)); - state.barrier.prev |= GPU_PHASE_COPY; - state.barrier.next |= GPU_PHASE_SHADER_VERTEX | GPU_PHASE_SHADER_FRAGMENT; - state.barrier.flush |= GPU_CACHE_TRANSFER_WRITE; - state.barrier.clear |= GPU_CACHE_UNIFORM; - data = staging.pointer; + block->head = block->nodes[block->head].next; + block->nodes[material->index].next = ~0u; + + mtx_unlock(&state.lock); + + return true; +} + +static void lovrMaterialRecycle(Material* material) { + if (!material->block) return; + mtx_lock(&state.lock); + material->block->nodes[material->index].tick = state.tick; + material->block->tail = material->index; + if (material->block->head == ~0u) material->block->head = material->block->tail; + material->block = NULL; + material->index = ~0u; + material->bundle = NULL; + mtx_unlock(&state.lock); +} + +static bool lovrMaterialUpload(Material* material) { + if (material->bufferTick == state.tick) { + return true; } - memcpy(data, info, sizeof(MaterialData)); + mtx_lock(&state.lock); + BufferView staging = getBuffer(GPU_BUFFER_STREAM, sizeof(MaterialData), 4); + if (!staging.buffer) return mtx_unlock(&state.lock), NULL; + gpu_copy_buffers(state.stream, staging.buffer, material->block->buffer, staging.offset, MATERIAL_STRIDE * material->index, sizeof(MaterialData)); + state.barrier.prev |= GPU_PHASE_COPY; + state.barrier.next |= GPU_PHASE_SHADER_VERTEX | GPU_PHASE_SHADER_FRAGMENT; + state.barrier.flush |= GPU_CACHE_TRANSFER_WRITE; + state.barrier.clear |= GPU_CACHE_UNIFORM; + material->bufferTick = state.tick; + material->pointer = staging.pointer; + memcpy(material->pointer, &material->data, sizeof(MaterialData)); + mtx_unlock(&state.lock); - gpu_buffer_binding buffer = { - .object = block->buffer, - .offset = material->index * stride, - .extent = stride - }; + return true; +} - gpu_binding bindings[8] = { - { 0, GPU_SLOT_UNIFORM_BUFFER, .buffer = buffer } +Material* lovrMaterialCreate(Texture* texture) { + Material* material = lovrCalloc(sizeof(Material)); + material->ref = 1; + + material->data = (MaterialData) { + .colors[COLOR_BASE] = { 1.f, 1.f, 1.f, 1.f }, + .quad = { 0.f, 0.f, 1.f, 1.f } }; - for (uint32_t i = 0; i < COUNTOF(textures); i++) { - Texture* texture = textures[i] ? textures[i] : state.defaultTexture; - bindings[i + 1] = (gpu_binding) { i + 1, GPU_SLOT_SAMPLED_TEXTURE, .texture.object = texture->sampleViewFloat }; - material->hasWritableTexture |= texture->info.usage != TEXTURE_SAMPLE; - lovrRetain(textures[i]); + for (uint32_t i = 0; i < TEXTURE_COUNT; i++) { + Texture* t = state.defaultTexture; + material->textures[i] = t; + material->bindings[i + 1] = (gpu_binding) { + .number = i + 1, + .type = GPU_SLOT_SAMPLED_TEXTURE, + .texture.object = t->sampleViewFloat + }; + lovrRetain(t); } - gpu_bundle_info bundleInfo = { - .layout = state.materialLayout->gpu, - .bindings = bindings, - .count = COUNTOF(bindings) - }; + if (texture) { + // This will take care of allocating the material and writing all the bindings + if (!lovrMaterialSetTexture(material, TEXTURE_COLOR, texture)) { + lovrFree(material); + return NULL; + } + } else { + if (!lovrMaterialAllocate(material)) { + lovrFree(material); + return NULL; + } - gpu_bundle_write(&material->bundle, &bundleInfo, 1); + gpu_bundle_info bundleInfo = { + .layout = state.materialLayout->gpu, + .bindings = material->bindings, + .count = COUNTOF(material->bindings) + }; + + gpu_bundle_write(&material->bundle, &bundleInfo, 1); + } + + if (material->pointer) { + memcpy(material->pointer, &material->data, sizeof(MaterialData)); + } else { + lovrMaterialUpload(material); + } - block->head = material->next; - material->next = ~0u; - mtx_unlock(&state.lock); return material; } void lovrMaterialDestroy(void* ref) { Material* material = ref; - mtx_lock(&state.lock); - material->tick = state.tick; - material->block->tail = material->index; - if (material->block->head == ~0u) material->block->head = material->block->tail; - mtx_unlock(&state.lock); - lovrRelease(material->info.texture, lovrTextureDestroy); - lovrRelease(material->info.glowTexture, lovrTextureDestroy); - lovrRelease(material->info.metalnessTexture, lovrTextureDestroy); - lovrRelease(material->info.roughnessTexture, lovrTextureDestroy); - lovrRelease(material->info.clearcoatTexture, lovrTextureDestroy); - lovrRelease(material->info.occlusionTexture, lovrTextureDestroy); - lovrRelease(material->info.normalTexture, lovrTextureDestroy); + lovrMaterialRecycle(material); + for (uint32_t i = 0; i < TEXTURE_COUNT; i++) { + lovrRelease(material->textures[i], lovrTextureDestroy); + } + lovrFree(material); +} + +float lovrMaterialGetNumber(Material* material, MaterialNumber key) { + return material->data.numbers[key]; +} + +bool lovrMaterialSetNumber(Material* material, MaterialNumber key, float number) { + if (!lovrMaterialUpload(material)) return false; + material->data.numbers[key] = number; + material->pointer->numbers[key] = number; + return true; +} + +const float* lovrMaterialGetColor(Material* material, MaterialColor key) { + return material->data.colors[key]; +} + +bool lovrMaterialSetColor(Material* material, MaterialColor key, float color[4]) { + if (!lovrMaterialUpload(material)) return false; + memcpy(material->data.colors[key], color, 4 * sizeof(float)); + material->pointer->colors[key][0] = lovrMathGammaToLinear(color[0]); + material->pointer->colors[key][1] = lovrMathGammaToLinear(color[1]); + material->pointer->colors[key][2] = lovrMathGammaToLinear(color[2]); + material->pointer->colors[key][3] = color[3]; + return true; +} + +Texture* lovrMaterialGetTexture(Material* material, MaterialTexture key) { + return material->textures[key]; +} + +bool lovrMaterialSetTexture(Material* material, MaterialTexture key, Texture* texture) { + if (texture == material->textures[key]) return true; + + lovrCheck(key == TEXTURE_COLOR || texture->info.type == TEXTURE_2D, "Material textures must be 2D"); + lovrCheck(texture->info.samples == 1, "Material textures can not be multisampled"); + lovrCheck(texture->info.usage & TEXTURE_SAMPLE, "Textures must be created with the 'sample' usage to use them in Materials"); + + lovrRelease(material->textures[key], lovrTextureDestroy); + material->textures[key] = texture; + material->bindings[key + 1].texture.object = texture->sampleViewFloat; + lovrRetain(texture); + + gpu_bundle_info bundleInfo = { .layout = state.materialLayout->gpu }; + + if (material->bundleTick == state.tick) { + bundleInfo.bindings = &material->bindings[key + 1]; + bundleInfo.count = 1; + } else { + lovrMaterialRecycle(material); + + if (!lovrMaterialAllocate(material)) { + return false; + } + + bundleInfo.bindings = material->bindings; + bundleInfo.count = COUNTOF(material->bindings); + } + + gpu_bundle_write(&material->bundle, &bundleInfo, 1); + + return true; } -const MaterialInfo* lovrMaterialGetInfo(Material* material) { - return &material->info; +void lovrMaterialGetQuad(Material* material, float* ox, float* oy, float* sx, float* sy) { + *ox = material->data.quad[0]; + *oy = material->data.quad[1]; + *sx = material->data.quad[2]; + *sy = material->data.quad[3]; +} + +bool lovrMaterialSetQuad(Material* material, float ox, float oy, float sx, float sy) { + if (!lovrMaterialUpload(material)) return false; + material->data.quad[0] = ox; + material->data.quad[1] = oy; + material->data.quad[2] = sx; + material->data.quad[3] = sy; + material->pointer->quad[0] = ox; + material->pointer->quad[1] = oy; + material->pointer->quad[2] = sx; + material->pointer->quad[3] = sy; + return true; +} + +static bool lovrMaterialSetSDFRange(Material* material, float x, float y) { + if (!lovrMaterialUpload(material)) return false; + material->data.sdfRange[0] = x; + material->data.sdfRange[1] = y; + material->pointer->sdfRange[0] = x; + material->pointer->sdfRange[1] = y; + return true; } // Font @@ -4344,11 +4456,7 @@ Font* lovrFontCreate(const FontInfo* info) { return NULL; } - font->material = lovrMaterialCreate(&(MaterialInfo) { - .data.color = { 1.f, 1.f, 1.f, 1.f }, - .data.uvScale = { 1.f, 1.f }, - .texture = font->atlas - }); + font->material = lovrMaterialCreate(font->atlas); if (!font->material) { lovrFontDestroy(font); @@ -4514,12 +4622,8 @@ static Glyph* lovrFontGetGlyph(Font* font, uint32_t codepoint, bool* resized) { return NULL; } - Material* material = lovrMaterialCreate(&(MaterialInfo) { - .data.color = { 1.f, 1.f, 1.f, 1.f }, - .data.uvScale = { 1.f, 1.f }, - .data.sdfRange = { font->info.spread / newWidth, font->info.spread / newHeight }, - .texture = atlas - }); + Material* material = lovrMaterialCreate(atlas); + lovrMaterialSetSDFRange(material, font->info.spread / newWidth, font->info.spread / newHeight); if (!material) { lovrTextureDestroy(atlas); @@ -5330,58 +5434,65 @@ Model* lovrModelCreate(const ModelInfo* info) { ModelData* data = info->data; ModelMetadata* meta = &model->meta; - // Materials and Textures + // Materials + + model->textures = lovrCalloc(meta->imageCount * sizeof(Texture*)); + model->materials = lovrCalloc(meta->materialCount * sizeof(Material*)); + if (info->materials) { - model->textures = lovrCalloc(meta->imageCount * sizeof(Texture*)); - model->materials = lovrMalloc(meta->materialCount * sizeof(Material*)); for (uint32_t i = 0; i < meta->materialCount; i++) { - MaterialInfo material; + Material* material = lovrMaterialCreate(NULL); + lovrAssertGoto(fail, material, "Failed to create model material: %s", lovrGetError()); + model->materials[i] = material; + ModelMaterial* properties = &meta->materials[i]; - memcpy(&material.data, properties, sizeof(MaterialData)); - - struct { uint32_t index; Texture** texture; } textures[] = { - { properties->texture, &material.texture }, - { properties->glowTexture, &material.glowTexture }, - { properties->metalnessTexture, &material.metalnessTexture }, - { properties->roughnessTexture, &material.roughnessTexture }, - { properties->clearcoatTexture, &material.clearcoatTexture }, - { properties->occlusionTexture, &material.occlusionTexture }, - { properties->normalTexture, &material.normalTexture } + lovrMaterialSetNumber(material, NUMBER_METALNESS, properties->metalness); + lovrMaterialSetNumber(material, NUMBER_ROUGHNESS, properties->roughness); + lovrMaterialSetNumber(material, NUMBER_CLEARCOAT, properties->clearcoat); + lovrMaterialSetNumber(material, NUMBER_CLEARCOAT_ROUGHNESS, properties->clearcoatRoughness); + lovrMaterialSetNumber(material, NUMBER_OCCLUSION_STRENGTH, properties->occlusionStrength); + lovrMaterialSetNumber(material, NUMBER_NORMAL_SCALE, properties->normalScale); + lovrMaterialSetNumber(material, NUMBER_ALPHA_CUTOFF, properties->alphaCutoff); + lovrMaterialSetColor(material, COLOR_BASE, properties->color); + lovrMaterialSetColor(material, COLOR_GLOW, properties->glow); + lovrMaterialSetQuad(material, properties->uvShift[0], properties->uvShift[1], properties->uvScale[0], properties->uvScale[1]); + + uint32_t textures[] = { + [TEXTURE_COLOR] = properties->texture, + [TEXTURE_GLOW] = properties->glowTexture, + [TEXTURE_METALNESS] = properties->metalnessTexture, + [TEXTURE_ROUGHNESS] = properties->roughnessTexture, + [TEXTURE_CLEARCOAT] = properties->clearcoatTexture, + [TEXTURE_OCCLUSION] = properties->occlusionTexture, + [TEXTURE_NORMAL] = properties->normalTexture }; for (uint32_t t = 0; t < COUNTOF(textures); t++) { - uint32_t index = textures[t].index; - Texture** texture = textures[t].texture; - - if (index == ~0u) { - *texture = NULL; - } else { - if (!model->textures[index]) { - Image* image = data->images[index]; - - model->textures[index] = lovrTextureCreate(&(TextureInfo) { - .type = TEXTURE_2D, - .usage = TEXTURE_SAMPLE, - .format = lovrImageGetFormat(image), - .width = lovrImageGetWidth(image, 0), - .height = lovrImageGetHeight(image, 0), - .layers = 1, - .mipmaps = info->mipmaps || lovrImageGetLevelCount(image) > 1 ? ~0u : 1, - .samples = 1, - .srgb = texture == &material.texture || texture == &material.glowTexture, - .images = &image, - .imageCount = 1 - }); - - if (!model->textures[index]) goto fail; - } - - *texture = model->textures[index]; + uint32_t index = textures[t]; + if (index == ~0u) continue; + + if (!model->textures[index]) { + Image* image = data->images[index]; + + model->textures[index] = lovrTextureCreate(&(TextureInfo) { + .type = TEXTURE_2D, + .usage = TEXTURE_SAMPLE, + .format = lovrImageGetFormat(image), + .width = lovrImageGetWidth(image, 0), + .height = lovrImageGetHeight(image, 0), + .layers = 1, + .mipmaps = info->mipmaps || lovrImageGetLevelCount(image) > 1 ? ~0u : 1, + .samples = 1, + .srgb = t == TEXTURE_COLOR || t == TEXTURE_GLOW, + .images = &image, + .imageCount = 1 + }); + + if (!model->textures[index]) goto fail; } - } - model->materials[i] = lovrMaterialCreate(&material); - lovrAssertGoto(fail, model->materials[i], "Failed to create model material: %s", lovrGetError()); + lovrMaterialSetTexture(material, t, model->textures[index]); + } } } @@ -5504,8 +5615,18 @@ Model* lovrModelClone(Model* parent) { ModelMetadata* meta = &model->meta; - model->textures = parent->textures; - model->materials = parent->materials; + model->textures = lovrMalloc(meta->imageCount * sizeof(Texture*)); + model->materials = lovrMalloc(meta->materialCount * sizeof(Material*)); + memcpy(model->textures, parent->textures, meta->imageCount * sizeof(Texture*)); + memcpy(model->materials, parent->materials, meta->materialCount * sizeof(Material*)); + + for (uint32_t i = 0; i < meta->imageCount; i++) { + lovrRetain(model->textures[i]); + } + + for (uint32_t i = 0; i < meta->materialCount; i++) { + lovrRetain(model->materials[i]); + } model->rawVertexBuffer = parent->rawVertexBuffer; model->indexBuffer = parent->indexBuffer; @@ -5571,16 +5692,14 @@ void lovrModelDestroy(void* ref) { return; } ModelMetadata* meta = &model->meta; - if (model->materials) { - for (uint32_t i = 0; i < meta->materialCount; i++) { - lovrRelease(model->materials[i], lovrMaterialDestroy); - } - for (uint32_t i = 0; i < meta->imageCount; i++) { - lovrRelease(model->textures[i], lovrTextureDestroy); - } - lovrFree(model->materials); - lovrFree(model->textures); + for (uint32_t i = 0; i < meta->materialCount; i++) { + lovrRelease(model->materials[i], lovrMaterialDestroy); + } + for (uint32_t i = 0; i < meta->imageCount; i++) { + lovrRelease(model->textures[i], lovrTextureDestroy); } + lovrFree(model->materials); + lovrFree(model->textures); if (model->meshes) { for (uint32_t i = 0; i < meta->meshCount; i++) { lovrRelease(model->meshes[i], lovrMeshDestroy); @@ -5833,7 +5952,7 @@ Mesh* lovrModelGetMesh(Model* model, uint32_t index) { default: lovrUnreachable(); } - if (model->materials && part->material != ~0u) { + if (part->material != ~0u) { lovrMeshSetMaterial(mesh, model->materials[part->material]); } @@ -5853,10 +5972,20 @@ Texture* lovrModelGetTexture(Model* model, uint32_t index) { return model->textures[index]; } -Material* lovrModelGetMaterial(Model* model, uint32_t index) { +bool lovrModelGetMaterial(Model* model, uint32_t index, Material** material) { uint32_t count = model->meta.materialCount; lovrCheck(index < count, "Invalid material index '%d' (Model has %d material%s)", index + 1, count, count == 1 ? "" : "s"); - return model->materials[index]; + *material = model->materials[index]; + return true; +} + +bool lovrModelSetMaterial(Model* model, uint32_t index, Material* material) { + uint32_t count = model->meta.materialCount; + lovrCheck(index < count, "Invalid material index '%d' (Model has %d material%s)", index + 1, count, count == 1 ? "" : "s"); + lovrRelease(model->materials[index], lovrMaterialDestroy); + model->materials[index] = material; + lovrRetain(material); + return true; } static bool lovrModelAnimateVertices(Model* model) { @@ -8965,7 +9094,7 @@ static bool drawNode(Pass* pass, Model* model, uint32_t index, uint32_t instance DrawInfo draw = { .mode = part->mode == DRAW_POINT_LIST ? DRAW_POINTS : part->mode == DRAW_LINE_LIST ? DRAW_LINES : DRAW_TRIANGLES, - .material = model->materials && part->material != ~0u ? model->materials[part->material] : NULL, + .material = part->material != ~0u ? model->materials[part->material] : NULL, .transform = node->skin == ~0u ? globalTransform : NULL, .bounds = bounds, .vertex.buffer = model->vertexBuffer, @@ -9042,7 +9171,7 @@ bool lovrPassDrawPart(Pass* pass, Model* model, uint32_t meshIndex, uint32_t par DrawInfo draw = { .mode = part->mode == DRAW_POINT_LIST ? DRAW_POINTS : part->mode == DRAW_LINE_LIST ? DRAW_LINES : DRAW_TRIANGLES, - .material = model->materials && part->material != ~0u ? model->materials[part->material] : NULL, + .material = part->material != ~0u ? model->materials[part->material] : NULL, .transform = transform, // TODO fix skinned mesh transforms? .bounds = part->bounds, .vertex.buffer = model->vertexBuffer, @@ -9771,20 +9900,16 @@ static void trackTexture(Pass* pass, Texture* texture, gpu_phase phase, gpu_cach } static void trackMaterial(Pass* pass, Material* material) { - if (!material->hasWritableTexture) { - return; - } - gpu_phase phase = GPU_PHASE_SHADER_VERTEX | GPU_PHASE_SHADER_FRAGMENT; gpu_cache cache = GPU_CACHE_TEXTURE; - trackTexture(pass, material->info.texture, phase, cache); - trackTexture(pass, material->info.glowTexture, phase, cache); - trackTexture(pass, material->info.metalnessTexture, phase, cache); - trackTexture(pass, material->info.roughnessTexture, phase, cache); - trackTexture(pass, material->info.clearcoatTexture, phase, cache); - trackTexture(pass, material->info.occlusionTexture, phase, cache); - trackTexture(pass, material->info.normalTexture, phase, cache); + trackTexture(pass, material->textures[TEXTURE_COLOR], phase, cache); + trackTexture(pass, material->textures[TEXTURE_GLOW], phase, cache); + trackTexture(pass, material->textures[TEXTURE_METALNESS], phase, cache); + trackTexture(pass, material->textures[TEXTURE_ROUGHNESS], phase, cache); + trackTexture(pass, material->textures[TEXTURE_CLEARCOAT], phase, cache); + trackTexture(pass, material->textures[TEXTURE_OCCLUSION], phase, cache); + trackTexture(pass, material->textures[TEXTURE_NORMAL], phase, cache); } static void trackRaytracer(Pass* pass, Raytracer* raytracer, gpu_phase phase, gpu_cache cache) { diff --git a/src/modules/graphics/graphics.h b/src/modules/graphics/graphics.h index b6172bd5f5..2d3507480b 100644 --- a/src/modules/graphics/graphics.h +++ b/src/modules/graphics/graphics.h @@ -379,35 +379,44 @@ const DataField* lovrShaderGetBufferFormat(Shader* shader, const char* name, uin // Material -typedef struct { - float color[4]; - float glow[4]; - float uvShift[2]; - float uvScale[2]; - float sdfRange[2]; - float metalness; - float roughness; - float clearcoat; - float clearcoatRoughness; - float occlusionStrength; - float normalScale; - float alphaCutoff; -} MaterialData; +typedef enum { + NUMBER_METALNESS, + NUMBER_ROUGHNESS, + NUMBER_CLEARCOAT, + NUMBER_CLEARCOAT_ROUGHNESS, + NUMBER_OCCLUSION_STRENGTH, + NUMBER_NORMAL_SCALE, + NUMBER_ALPHA_CUTOFF, + NUMBER_COUNT +} MaterialNumber; -typedef struct { - MaterialData data; - Texture* texture; - Texture* glowTexture; - Texture* metalnessTexture; - Texture* roughnessTexture; - Texture* clearcoatTexture; - Texture* occlusionTexture; - Texture* normalTexture; -} MaterialInfo; - -Material* lovrMaterialCreate(const MaterialInfo* info); +typedef enum { + COLOR_BASE, + COLOR_GLOW, + COLOR_COUNT +} MaterialColor; + +typedef enum { + TEXTURE_COLOR, + TEXTURE_GLOW, + TEXTURE_METALNESS, + TEXTURE_ROUGHNESS, + TEXTURE_CLEARCOAT, + TEXTURE_OCCLUSION, + TEXTURE_NORMAL, + TEXTURE_COUNT +} MaterialTexture; + +Material* lovrMaterialCreate(Texture* texture); void lovrMaterialDestroy(void* ref); -const MaterialInfo* lovrMaterialGetInfo(Material* material); +float lovrMaterialGetNumber(Material* material, MaterialNumber key); +bool lovrMaterialSetNumber(Material* material, MaterialNumber key, float number); +const float* lovrMaterialGetColor(Material* material, MaterialColor key); +bool lovrMaterialSetColor(Material* material, MaterialColor key, float color[4]); +Texture* lovrMaterialGetTexture(Material* material, MaterialTexture key); +bool lovrMaterialSetTexture(Material* material, MaterialTexture key, Texture* texture); +void lovrMaterialGetQuad(Material* material, float* ox, float* oy, float* sx, float* sy); +bool lovrMaterialSetQuad(Material* material, float ox, float oy, float sx, float sy); // Font @@ -536,7 +545,8 @@ Buffer* lovrModelGetVertexBuffer(Model* model); Buffer* lovrModelGetIndexBuffer(Model* model); Mesh* lovrModelGetMesh(Model* model, uint32_t index); Texture* lovrModelGetTexture(Model* model, uint32_t index); -Material* lovrModelGetMaterial(Model* model, uint32_t index); +bool lovrModelGetMaterial(Model* model, uint32_t index, Material** material); +bool lovrModelSetMaterial(Model* model, uint32_t index, Material* material); bool lovrModelBuildRaytracer(Model* model); // Raytracer From 7d7f7a22a3050b590f9a7ef3077c5073a18f1b89 Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 10 Aug 2026 12:37:40 -0700 Subject: [PATCH 2/7] Adjust material defaults; --- src/modules/graphics/graphics.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/graphics/graphics.c b/src/modules/graphics/graphics.c index ba7075425b..1404a6cdab 100644 --- a/src/modules/graphics/graphics.c +++ b/src/modules/graphics/graphics.c @@ -4243,6 +4243,8 @@ Material* lovrMaterialCreate(Texture* texture) { material->ref = 1; material->data = (MaterialData) { + .numbers[NUMBER_OCCLUSION_STRENGTH] = 1.f, + .numbers[NUMBER_NORMAL_SCALE] = 1.f, .colors[COLOR_BASE] = { 1.f, 1.f, 1.f, 1.f }, .quad = { 0.f, 0.f, 1.f, 1.f } }; From 8c23b268cd09bef54fda90e1bf4a5ba00914cfbe Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 10 Aug 2026 13:46:45 -0700 Subject: [PATCH 3/7] Fixes; --- src/api/l_graphics.c | 7 ++++--- src/api/l_graphics_material.c | 6 +++--- src/modules/graphics/graphics.c | 20 +++++++++++++++----- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/api/l_graphics.c b/src/api/l_graphics.c index 60594103cb..aee43b56a8 100644 --- a/src/api/l_graphics.c +++ b/src/api/l_graphics.c @@ -1491,8 +1491,6 @@ static int l_lovrGraphicsNewMaterial(lua_State* L) { luax_assert(L, material); if (lua_istable(L, 1)) { - float value, color[4]; - for (uint32_t i = 0; i < NUMBER_COUNT; i++) { lua_pushlstring(L, lovrMaterialNumber[i].string, lovrMaterialNumber[i].length); lua_gettable(L, 1); @@ -1509,6 +1507,7 @@ static int l_lovrGraphicsNewMaterial(lua_State* L) { lua_gettable(L, 1); if (!lua_isnil(L, -1)) { + float color[4]; luax_optcolor(L, -1, color); lovrMaterialSetColor(material, i, color); } @@ -1536,7 +1535,7 @@ static int l_lovrGraphicsNewMaterial(lua_State* L) { lua_pop(L, 1); } - float quad[4]; + float quad[4] = { 0.f, 0.f, 1.f, 1.f }; // Deprecated lua_getfield(L, 1, "uvShift"); @@ -1577,6 +1576,8 @@ static int l_lovrGraphicsNewMaterial(lua_State* L) { lua_pop(L, 4); } lua_pop(L, 1); + + lovrMaterialSetQuad(material, quad[0], quad[1], quad[2], quad[3]); } luax_pushtype(L, Material, material); diff --git a/src/api/l_graphics_material.c b/src/api/l_graphics_material.c index be0bea126e..11acac4b9a 100644 --- a/src/api/l_graphics_material.c +++ b/src/api/l_graphics_material.c @@ -29,7 +29,7 @@ static int l_lovrMaterialSetNumber(lua_State* L) { Material* material = luax_checktype(L, 1, Material); MaterialNumber key = luax_checkenum(L, 2, MaterialNumber, NULL); float number = luax_checkfloat(L, 3); - lovrMaterialSetNumber(material, key, number); + luax_assert(L, lovrMaterialSetNumber(material, key, number)); return 0; } @@ -50,7 +50,7 @@ static int l_lovrMaterialSetColor(lua_State* L) { float color[4]; MaterialColor key = lua_type(L, index) == LUA_TSTRING ? luax_checkenum(L, index++, MaterialColor, NULL) : COLOR_BASE; luax_readcolor(L, index, color); - lovrMaterialSetColor(material, key, color); + luax_assert(L, lovrMaterialSetColor(material, key, color)); return 0; } @@ -67,7 +67,7 @@ static int l_lovrMaterialSetTexture(lua_State* L) { int index = 2; MaterialTexture key = lua_type(L, index) == LUA_TSTRING ? luax_checkenum(L, index++, MaterialTexture, NULL) : TEXTURE_COLOR; Texture* texture = luax_checktype(L, index, Texture); - lovrMaterialSetTexture(material, key, texture); + luax_assert(L, lovrMaterialSetTexture(material, key, texture)); return 0; } diff --git a/src/modules/graphics/graphics.c b/src/modules/graphics/graphics.c index 1404a6cdab..cc098d772a 100644 --- a/src/modules/graphics/graphics.c +++ b/src/modules/graphics/graphics.c @@ -4208,9 +4208,11 @@ static bool lovrMaterialAllocate(Material* material) { static void lovrMaterialRecycle(Material* material) { if (!material->block) return; mtx_lock(&state.lock); - material->block->nodes[material->index].tick = state.tick; - material->block->tail = material->index; - if (material->block->head == ~0u) material->block->head = material->block->tail; + MaterialBlock* block = material->block; + block->nodes[material->index].tick = state.tick; + if (block->head == ~0u) block->head = material->index; + else block->nodes[block->tail].next = material->index; + block->tail = material->index; material->block = NULL; material->index = ~0u; material->bundle = NULL; @@ -4224,7 +4226,7 @@ static bool lovrMaterialUpload(Material* material) { mtx_lock(&state.lock); BufferView staging = getBuffer(GPU_BUFFER_STREAM, sizeof(MaterialData), 4); - if (!staging.buffer) return mtx_unlock(&state.lock), NULL; + if (!staging.buffer) return mtx_unlock(&state.lock), false; gpu_copy_buffers(state.stream, staging.buffer, material->block->buffer, staging.offset, MATERIAL_STRIDE * material->index, sizeof(MaterialData)); state.barrier.prev |= GPU_PHASE_COPY; state.barrier.next |= GPU_PHASE_SHADER_VERTEX | GPU_PHASE_SHADER_FRAGMENT; @@ -4232,7 +4234,15 @@ static bool lovrMaterialUpload(Material* material) { state.barrier.clear |= GPU_CACHE_UNIFORM; material->bufferTick = state.tick; material->pointer = staging.pointer; - memcpy(material->pointer, &material->data, sizeof(MaterialData)); + memcpy(material->pointer->numbers, &material->data.numbers, sizeof(material->data.numbers)); + for (uint32_t i = 0; i < COLOR_COUNT; i++) { + material->pointer->colors[i][0] = lovrMathGammaToLinear(material->data.colors[i][0]); + material->pointer->colors[i][1] = lovrMathGammaToLinear(material->data.colors[i][1]); + material->pointer->colors[i][2] = lovrMathGammaToLinear(material->data.colors[i][2]); + material->pointer->colors[i][3] = material->data.colors[i][3]; + } + memcpy(material->pointer->quad, material->data.quad, sizeof(material->data.quad)); + memcpy(material->pointer->sdfRange, material->data.sdfRange, sizeof(material->data.sdfRange)); mtx_unlock(&state.lock); return true; From 8cbb4ee859ef71fefaca157f777a71039fef2cbe Mon Sep 17 00:00:00 2001 From: bjorn Date: Sat, 15 Aug 2026 12:48:18 -0700 Subject: [PATCH 4/7] ModelData uses new quad field; --- src/api/l_data_modelData.c | 19 ++++++++----------- src/modules/data/modelData.c | 7 +++---- src/modules/data/modelData.h | 9 ++++----- src/modules/data/modelData_gltf.c | 8 ++++---- src/modules/data/modelData_obj.c | 7 +++---- src/modules/graphics/graphics.c | 2 +- 6 files changed, 23 insertions(+), 29 deletions(-) diff --git a/src/api/l_data_modelData.c b/src/api/l_data_modelData.c index 6c830fbc74..cb04603445 100644 --- a/src/api/l_data_modelData.c +++ b/src/api/l_data_modelData.c @@ -602,19 +602,16 @@ static int l_lovrModelDataGetMaterial(lua_State* L) { lua_rawseti(L, -2, 4); lua_setfield(L, -2, "glow"); - lua_createtable(L, 2, 0); - lua_pushnumber(L, material->uvShift[0]); - lua_rawseti(L, -2, 1); - lua_pushnumber(L, material->uvShift[1]); - lua_rawseti(L, -2, 2); - lua_setfield(L, -2, "uvShift"); - - lua_createtable(L, 2, 0); - lua_pushnumber(L, material->uvScale[0]); + lua_createtable(L, 4, 0); + lua_pushnumber(L, material->quad[0]); lua_rawseti(L, -2, 1); - lua_pushnumber(L, material->uvScale[1]); + lua_pushnumber(L, material->quad[1]); lua_rawseti(L, -2, 2); - lua_setfield(L, -2, "uvScale"); + lua_pushnumber(L, material->quad[2]); + lua_rawseti(L, -2, 3); + lua_pushnumber(L, material->quad[3]); + lua_rawseti(L, -2, 4); + lua_setfield(L, -2, "quad"); lua_pushnumber(L, material->metalness), lua_setfield(L, -2, "metalness"); lua_pushnumber(L, material->roughness), lua_setfield(L, -2, "roughness"); diff --git a/src/modules/data/modelData.c b/src/modules/data/modelData.c index 53a4fdf4b5..f2003e3dea 100644 --- a/src/modules/data/modelData.c +++ b/src/modules/data/modelData.c @@ -109,10 +109,6 @@ void lovrModelDataAllocate(ModelData* model) { for (uint32_t i = 0; i < meta->materialCount; i++) { meta->materials[i] = (ModelMaterial) { - .color = { 1.f, 1.f, 1.f, 1.f }, - .glow = { 0.f, 0.f, 0.f, 1.f }, - .uvShift = { 0.f, 0.f }, - .uvScale = { 1.f, 1.f }, .metalness = 1.f, .roughness = 1.f, .clearcoat = 0.f, @@ -120,6 +116,9 @@ void lovrModelDataAllocate(ModelData* model) { .occlusionStrength = 1.f, .normalScale = 1.f, .alphaCutoff = 0.f, + .color = { 1.f, 1.f, 1.f, 1.f }, + .glow = { 0.f, 0.f, 0.f, 1.f }, + .quad = { 0.f, 0.f, 1.f, 1.f }, .texture = ~0u, .glowTexture = ~0u, .metalnessTexture = ~0u, diff --git a/src/modules/data/modelData.h b/src/modules/data/modelData.h index 6843151e0b..7702cb03ac 100644 --- a/src/modules/data/modelData.h +++ b/src/modules/data/modelData.h @@ -74,11 +74,6 @@ typedef struct { } ModelMesh; typedef struct { - float color[4]; - float glow[4]; - float uvShift[2]; - float uvScale[2]; - float sdfRange[2]; float metalness; float roughness; float clearcoat; @@ -86,6 +81,10 @@ typedef struct { float occlusionStrength; float normalScale; float alphaCutoff; + float color[4]; + float glow[4]; + float quad[4]; + float sdfRange[2]; uint32_t texture; uint32_t glowTexture; uint32_t metalnessTexture; diff --git a/src/modules/data/modelData_gltf.c b/src/modules/data/modelData_gltf.c index 8786522fe0..d2ff85c6c1 100644 --- a/src/modules/data/modelData_gltf.c +++ b/src/modules/data/modelData_gltf.c @@ -179,12 +179,12 @@ static jsmntok_t* nomTexture(const char* json, jsmntok_t* token, ModelMaterial* gltfString key = NOM_STR(json, token); if (STR_EQ(key, "offset")) { token++; // Enter array - material->uvShift[0] = NOM_FLOAT(json, token); - material->uvShift[1] = NOM_FLOAT(json, token); + material->quad[0] = NOM_FLOAT(json, token); + material->quad[1] = NOM_FLOAT(json, token); } else if (STR_EQ(key, "scale")) { token++; // Enter array - material->uvScale[0] = NOM_FLOAT(json, token); - material->uvScale[1] = NOM_FLOAT(json, token); + material->quad[2] = NOM_FLOAT(json, token); + material->quad[3] = NOM_FLOAT(json, token); } else { token = NOM(token); } diff --git a/src/modules/data/modelData_obj.c b/src/modules/data/modelData_obj.c index c1ce151aec..0a7babf9ca 100644 --- a/src/modules/data/modelData_obj.c +++ b/src/modules/data/modelData_obj.c @@ -58,10 +58,6 @@ static bool parseMtl(char* path, char* base, ModelDataIO* io, arr_image_t* image if (STARTS_WITH(line, "newmtl ")) { map_set(names, hash64(line + 7, length - 7), materials->length); arr_push(materials, ((ModelMaterial) { - .color = { 1.f, 1.f, 1.f, 1.f }, - .glow = { 0.f, 0.f, 0.f, 1.f }, - .uvShift = { 0.f, 1.f }, - .uvScale = { 1.f, 1.f }, .metalness = 1.f, .roughness = 1.f, .clearcoat = 0.f, @@ -69,6 +65,9 @@ static bool parseMtl(char* path, char* base, ModelDataIO* io, arr_image_t* image .occlusionStrength = 1.f, .normalScale = 1.f, .alphaCutoff = 0.f, + .color = { 1.f, 1.f, 1.f, 1.f }, + .glow = { 0.f, 0.f, 0.f, 1.f }, + .quad = { 0.f, 1.f, 1.f, 1.f }, .texture = ~0u, .glowTexture = ~0u, .metalnessTexture = ~0u, diff --git a/src/modules/graphics/graphics.c b/src/modules/graphics/graphics.c index cc098d772a..012496593f 100644 --- a/src/modules/graphics/graphics.c +++ b/src/modules/graphics/graphics.c @@ -5467,7 +5467,7 @@ Model* lovrModelCreate(const ModelInfo* info) { lovrMaterialSetNumber(material, NUMBER_ALPHA_CUTOFF, properties->alphaCutoff); lovrMaterialSetColor(material, COLOR_BASE, properties->color); lovrMaterialSetColor(material, COLOR_GLOW, properties->glow); - lovrMaterialSetQuad(material, properties->uvShift[0], properties->uvShift[1], properties->uvScale[0], properties->uvScale[1]); + lovrMaterialSetQuad(material, properties->quad[0], properties->quad[1], properties->quad[0], properties->quad[1]); uint32_t textures[] = { [TEXTURE_COLOR] = properties->texture, From 532a3b255769338622cbd5232f1db9d435b097bf Mon Sep 17 00:00:00 2001 From: bjorn Date: Sat, 15 Aug 2026 15:20:44 -0700 Subject: [PATCH 5/7] Material:is/setDoubleSided and auto cull mode; --- src/api/l_data_modelData.c | 2 ++ src/api/l_graphics_material.c | 16 ++++++++++++++++ src/api/l_graphics_pass.c | 6 ++++-- src/modules/data/modelData.h | 1 + src/modules/data/modelData_gltf.c | 2 ++ src/modules/graphics/graphics.c | 27 ++++++++++++++++++++++++--- src/modules/graphics/graphics.h | 5 ++++- 7 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/api/l_data_modelData.c b/src/api/l_data_modelData.c index cb04603445..b1a3051c69 100644 --- a/src/api/l_data_modelData.c +++ b/src/api/l_data_modelData.c @@ -630,6 +630,8 @@ static int l_lovrModelDataGetMaterial(lua_State* L) { PUSH_IMAGE(occlusionTexture); PUSH_IMAGE(normalTexture); + lua_pushboolean(L, material->doubleSided), lua_setfield(L, -2, "doubleSided"); + return 1; } diff --git a/src/api/l_graphics_material.c b/src/api/l_graphics_material.c index 11acac4b9a..6fe009e6c3 100644 --- a/src/api/l_graphics_material.c +++ b/src/api/l_graphics_material.c @@ -92,6 +92,20 @@ static int l_lovrMaterialSetQuad(lua_State* L) { return 0; } +static int l_lovrMaterialIsDoubleSided(lua_State* L) { + Material* material = luax_checktype(L, 1, Material); + bool doubleSided = lovrMaterialIsDoubleSided(material); + lua_pushboolean(L, doubleSided); + return 1; +} + +static int l_lovrMaterialSetDoubleSided(lua_State* L) { + Material* material = luax_checktype(L, 1, Material); + bool doubleSided = lua_toboolean(L, 2); + lovrMaterialSetDoubleSided(material, doubleSided); + return 0; +} + // Deprecated static int l_lovrMaterialGetProperties(lua_State* L) { Material* material = luax_checktype(L, 1, Material); @@ -170,6 +184,8 @@ const luaL_Reg lovrMaterial[] = { { "setTexture", l_lovrMaterialSetTexture }, { "getQuad", l_lovrMaterialGetQuad }, { "setQuad", l_lovrMaterialSetQuad }, + { "isDoubleSided", l_lovrMaterialIsDoubleSided }, + { "setDoubleSided", l_lovrMaterialSetDoubleSided }, // Deprecated { "getProperties", l_lovrMaterialGetProperties }, diff --git a/src/api/l_graphics_pass.c b/src/api/l_graphics_pass.c index b45a548e7e..7aa93d9976 100644 --- a/src/api/l_graphics_pass.c +++ b/src/api/l_graphics_pass.c @@ -617,10 +617,12 @@ static int l_lovrPassSetDepthClamp(lua_State* L) { static int l_lovrPassSetFaceCull(lua_State* L) { Pass* pass = luax_checktype(L, 1, Pass); CullMode mode; - if (lua_type(L, 2) == LUA_TBOOLEAN) { + if (lua_isnoneornil(L, 2)) { + mode = CULL_AUTO; + } else if (lua_type(L, 2) == LUA_TBOOLEAN) { mode = lua_toboolean(L, 2) ? CULL_BACK : CULL_NONE; } else { - mode = luax_checkenum(L, 2, CullMode, "none"); + mode = luax_checkenum(L, 2, CullMode, NULL); } lovrPassSetFaceCull(pass, mode); return 0; diff --git a/src/modules/data/modelData.h b/src/modules/data/modelData.h index 7702cb03ac..8666b6f84a 100644 --- a/src/modules/data/modelData.h +++ b/src/modules/data/modelData.h @@ -92,6 +92,7 @@ typedef struct { uint32_t clearcoatTexture; uint32_t occlusionTexture; uint32_t normalTexture; + bool doubleSided; const char* name; } ModelMaterial; diff --git a/src/modules/data/modelData_gltf.c b/src/modules/data/modelData_gltf.c index d2ff85c6c1..b9a2c661d8 100644 --- a/src/modules/data/modelData_gltf.c +++ b/src/modules/data/modelData_gltf.c @@ -1049,6 +1049,8 @@ bool lovrModelDataInitGltf(ModelData** result, Blob* source, ModelDataIO* io) { material->glow[2] = NOM_FLOAT(json, token); } else if (STR_EQ(key, "alphaCutoff")) { material->alphaCutoff = NOM_FLOAT(json, token); + } else if (STR_EQ(key, "doubleSided")) { + material->doubleSided = NOM_BOOL(json, token); } else if (STR_EQ(key, "name")) { gltfString name = NOM_STR(json, token); meta->materialLookup[material - meta->materials] = (uint32_t) hash64(name.data, name.length); diff --git a/src/modules/graphics/graphics.c b/src/modules/graphics/graphics.c index 012496593f..1543f484ad 100644 --- a/src/modules/graphics/graphics.c +++ b/src/modules/graphics/graphics.c @@ -207,6 +207,7 @@ struct Material { uint32_t index; MaterialBlock* block; MaterialData data; + bool doubleSided; Texture* textures[TEXTURE_COUNT]; gpu_binding bindings[1 + TEXTURE_COUNT]; gpu_bundle* bundle; @@ -453,6 +454,7 @@ typedef struct { typedef struct { bool dirty; bool viewCull; + CullMode faceCull; DrawMode mode; float color[4]; Buffer* lastVertexBuffer; @@ -4391,6 +4393,14 @@ bool lovrMaterialSetQuad(Material* material, float ox, float oy, float sx, float return true; } +bool lovrMaterialIsDoubleSided(Material* material) { + return material->doubleSided; +} + +void lovrMaterialSetDoubleSided(Material* material, bool doubleSided) { + material->doubleSided = doubleSided; +} + static bool lovrMaterialSetSDFRange(Material* material, float x, float y) { if (!lovrMaterialUpload(material)) return false; material->data.sdfRange[0] = x; @@ -5467,7 +5477,8 @@ Model* lovrModelCreate(const ModelInfo* info) { lovrMaterialSetNumber(material, NUMBER_ALPHA_CUTOFF, properties->alphaCutoff); lovrMaterialSetColor(material, COLOR_BASE, properties->color); lovrMaterialSetColor(material, COLOR_GLOW, properties->glow); - lovrMaterialSetQuad(material, properties->quad[0], properties->quad[1], properties->quad[0], properties->quad[1]); + lovrMaterialSetQuad(material, properties->quad[0], properties->quad[1], properties->quad[2], properties->quad[3]); + lovrMaterialSetDoubleSided(material, properties->doubleSided); uint32_t textures[] = { [TEXTURE_COLOR] = properties->texture, @@ -6827,6 +6838,7 @@ void lovrPassReset(Pass* pass) { pass->pipelineIndex = 0; memset(pass->pipeline, 0, sizeof(Pipeline)); + pass->pipeline->faceCull = CULL_AUTO; pass->pipeline->mode = DRAW_TRIANGLES; pass->pipeline->lastVertexFormat = ~0u; pass->pipeline->color[0] = 1.f; @@ -7430,8 +7442,11 @@ void lovrPassSetDepthClamp(Pass* pass, bool clamp) { } void lovrPassSetFaceCull(Pass* pass, CullMode mode) { - pass->pipeline->dirty |= pass->pipeline->info.rasterizer.cullMode != (gpu_cull_mode) mode; - pass->pipeline->info.rasterizer.cullMode = (gpu_cull_mode) mode; + if (mode != CULL_AUTO) { + pass->pipeline->dirty |= pass->pipeline->info.rasterizer.cullMode != (gpu_cull_mode) mode; + pass->pipeline->info.rasterizer.cullMode = (gpu_cull_mode) mode; + } + pass->pipeline->faceCull = mode; } void lovrPassSetFont(Pass* pass, Font* font) { @@ -7845,6 +7860,12 @@ static void lovrPassResolvePipeline(Pass* pass, DrawInfo* info, Draw* draw, Draw pipeline->dirty = true; } + if (pipeline->faceCull == CULL_AUTO) { + gpu_cull_mode cullMode = draw->material->doubleSided ? GPU_CULL_NONE : GPU_CULL_BACK; + pipeline->dirty |= cullMode != pipeline->info.rasterizer.cullMode; + pipeline->info.rasterizer.cullMode = cullMode; + } + // Vertex formats if (info->vertex.buffer && pipeline->lastVertexBuffer != info->vertex.buffer) { pipeline->lastVertexFormat = ~0u; diff --git a/src/modules/graphics/graphics.h b/src/modules/graphics/graphics.h index 2d3507480b..2ef05cc005 100644 --- a/src/modules/graphics/graphics.h +++ b/src/modules/graphics/graphics.h @@ -417,6 +417,8 @@ Texture* lovrMaterialGetTexture(Material* material, MaterialTexture key); bool lovrMaterialSetTexture(Material* material, MaterialTexture key, Texture* texture); void lovrMaterialGetQuad(Material* material, float* ox, float* oy, float* sx, float* sy); bool lovrMaterialSetQuad(Material* material, float ox, float oy, float sx, float sy); +bool lovrMaterialIsDoubleSided(Material* material); +void lovrMaterialSetDoubleSided(Material* material, bool doubleSided); // Font @@ -675,7 +677,8 @@ typedef struct { typedef enum { CULL_NONE, CULL_FRONT, - CULL_BACK + CULL_BACK, + CULL_AUTO } CullMode; typedef enum { From cc6506dfab1f18c2128b01be56f1734782ae7493 Mon Sep 17 00:00:00 2001 From: bjorn Date: Sat, 15 Aug 2026 17:52:11 -0700 Subject: [PATCH 6/7] Update changelog; --- CHANGES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 9051271da0..e95aa8377b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,7 +20,9 @@ dev - Add `sleep` callback to `World:setCallbacks`. - Add `linearDamping` and `angularDamping` settings to `lovr.physics.newWorld`. - Add `lovr.headset.getHandPosition/Orientation/Pose`. -- Add `Material:get/setNumber`, `Material:get/setColor`, `Material:get/setTexture`, and `Material:get/setQuad`. +- Add `Material:get/setNumber`, `Material:get/setColor`, and `Material:get/setTexture`. +- Add `Material:get/setQuad`. +- Add `Material:is/setDoubleSided`. ### Change From 7ce0fb2fbc42e9aade91810fafc50f94e3ef0936 Mon Sep 17 00:00:00 2001 From: bjorn Date: Fri, 21 Aug 2026 13:19:57 -0700 Subject: [PATCH 7/7] Disable culling by default; --- src/modules/graphics/graphics.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/graphics/graphics.c b/src/modules/graphics/graphics.c index 1543f484ad..4b73a6608c 100644 --- a/src/modules/graphics/graphics.c +++ b/src/modules/graphics/graphics.c @@ -6838,7 +6838,6 @@ void lovrPassReset(Pass* pass) { pass->pipelineIndex = 0; memset(pass->pipeline, 0, sizeof(Pipeline)); - pass->pipeline->faceCull = CULL_AUTO; pass->pipeline->mode = DRAW_TRIANGLES; pass->pipeline->lastVertexFormat = ~0u; pass->pipeline->color[0] = 1.f;