local utils = require "utils" local material_properties = {} material_properties.MATERIALID_DIFFUSE_ALBEDO = 0 material_properties.MATERIALID_DIFFUSE_ALBEDO_MULTIPLIER_MASK = 1 material_properties.MATERIALID_NORMAL = 2 material_properties.MATERIALID_SMOOTHNESS = 3 material_properties.MATERIALID_SPECULAR_ALBEDO = 4 material_properties.MATERIALID_EMISSION = 5 material_properties.MATERIALID_OPACITY = 6 material_properties.MATERIALID_HEIGHT = 7 material_properties.MATERIALID_USER_START = 8 -- Initialize name table as stack for starting/ending property groups local current_parent = { "", "" } -- Set to keep track of the defined properties for validation local property_names = {} property_names[""] = true function material_properties.start_property_group( group ) assert( current_parent[#current_parent] ~= group, "Attempting to start duplicate group " .. group .. "!" ) -- push on to stack current_parent[#current_parent + 1] = group end function material_properties.end_property_group( group ) assert( current_parent[#current_parent] == group, "Attempting to end different group '" .. group .. "' than current one" .. current_parent[#current_parent] .. "'!" ) -- pop from stack current_parent[#current_parent] = nil end -- Used for validation of property bits, so that each parameter defines a unique property bit local property_bits_in_use = {} -- Flag used to indicate if a property flag is being constructed local constructing_property_array = false local temp_property_array_description = nil function material_properties.start_property_array(array_name, element_name, max_array_size) -- override assert and error so that any errors are propagated to the material script local error = function(msg) error(msg, 3) end local assert = function(cond, msg) if not cond then return error(msg) end end assert(not constructing_property_array, "Attempting to construct another property array while one is already being constructed!") constructing_property_array = true temp_property_array_values = {} temp_property_array_description = { name = array_name, type = "property_array", editor_type = "default", array_max_size = array_max_size, default = {}, values = {}, editor_is_visible = true, runtime_include = true } -- generate serialized flag if temp_property_array_description.serialized == nil then temp_property_array_description.serialized = true end -- generate ui_name temp_property_array_description.ui_name = temp_property_array_description.ui_name or string.gsub(temp_property_array_description.name, "(%l)(%u%l)", "%1 %2") -- generate parent if none is given temp_property_array_description.parent = temp_property_array_description.parent or current_parent[#current_parent] assert(property_names[temp_property_array_description.parent], "Attempting to parent property '".. temp_property_array_description.parent .."' that does not exist!" ) -- make sure that a property cannot parent itself, assign the previous parent instead if temp_property_array_description.parent == temp_property_array_description.name then temp_property_array_description.parent = current_parent[#current_parent - 1] end end function material_properties.add_array_property(desc) temp_property_array_description.values[#temp_property_array_description.values + 1] = desc end function material_properties.end_property_array(array_name) assert(#temp_property_array_description.values > 0, "Trying to create empty property array!") add_material_property_array( temp_property_array_description ) temp_property_array_values = nil temp_property_array_description = nil constructing_property_array = false end function material_properties.property(desc) local types = utils.make_set{ "bool", "int", "float", "float2", "float3", "float4", "enum", "texture2d", "group", "material", "property_array" } local defaults = { bool = false, int = 0, float = 0.0, float2 = vector(0.0, 0.0), float3 = vector(0.0, 0.0, 0.0), float4 = vector(0.0, 0.0, 0.0, 0.0), } local field_types = { name = "string", type = "string", enum_names = "table", enum_values = "table", property_bit = "number", property_switch = "table", validation_range = "table", editor_range = "table", editor_type = "string", editor_read_only = "boolean", editor_is_visible = "boolean", parent = "string", serialized = "boolean", runtime_skip = "boolean" } -- override assert and error so that any errors are propagated to the material script local assert = function(cond, msg) if not cond then error(msg, 3) end end local error = function(msg) error(msg, 3) end -- validate parameter types assert( desc.name, "missing field 'name'" ) assert( type(desc.name) == "string", "invalid value defined for field 'name'" ) assert( not property_names[desc.name], "attempting to add property with duplicate name '".. desc.name .."'!" ) property_names[desc.name] = true assert(desc.type, "missing field 'type'") assert(types[desc.type], "invalid value for 'type' (bool, int, float, float2, float3, float4, enum, texture2d, material, property_array or group expected)") -- validate type of fields for field, expected_type in pairs(field_types) do if desc[field] and type(desc[field]) ~= expected_type then error("invalid type for '" .. field .."' (" .. expected_type .. " expected)") end end -- generate serialized flag if desc.serialized == nil then desc.serialized = true end -- generate runtime_include flag if desc.runtime_include == nil then desc.runtime_include = true end -- generate ui_name desc.ui_name = desc.ui_name or string.gsub(desc.name, "(%l)(%u%l)", "%1 %2") -- generate default desc.default = desc.default or defaults[desc.type] if desc.type == "enum" and desc.default == nil and desc.enum_values then desc.default = desc.enum_values[1] end if desc.type == "group" and desc.default == nil then desc.default = "" end if desc.type == "material" and desc.default == nil then desc.default = "" end assert(desc.default ~= nil, "missing field 'default'") -- generate parent if none is given desc.parent = desc.parent or current_parent[#current_parent] assert(property_names[desc.parent], "Attempting to parent property '".. desc.parent .."' that does not exist!" ) -- make sure that a property cannot parent itself, assign the previous parent instead if desc.parent == desc.name then desc.parent = current_parent[#current_parent - 1] end assert(desc.parent ~= nil, "missing field 'parent'") assert(desc.parent ~= desc.name, "field 'parent' and field 'name' are the same!") -- generate enum_names if desc.enum_values and desc.enum_names == nil then desc.enum_names = utils.copy(desc.enum_values) for i = 1, #desc.enum_values do desc.enum_names[i] = utils.snake_to_title_case(desc.enum_values[i]) end end -- generate editor_is_visible flag if desc.editor_is_visible == nil then desc.editor_is_visible = true end -- validate enum_values and enum_names if desc.type == "enum" then assert(desc.enum_values, "missing field 'enum_values'") assert(#desc.enum_values == #desc.enum_names, "'enum_values' or 'enum_names' has invalid number of elements") local valid_values = {} for i = 1, #desc.enum_values do assert(type(desc.enum_values[i]) == "string", "invalid enum values") assert(type(desc.enum_names[i]) == "string", "invalid enum names") valid_values[desc.enum_values[i]] = true end assert(valid_values[desc.default], "property '".. desc.name .."' default value '".. desc.default .."' is not valid!"); end -- validate validation_range if desc.validation_range then assert(#desc.validation_range == 2 and type(desc.validation_range[1]) == "number" and type(desc.validation_range[2]) == "number", "invalid validation_range") end -- validate editor_range if desc.editor_range then assert(#desc.editor_range == 2 and type(desc.editor_range[1]) == "number" and type(desc.editor_range[2]) == "number", "invalid editor_range") end -- validate property_bit if desc.property_bit ~= nil and desc.property_bit >= 0 then assert( not property_bits_in_use[ desc.property_bit ], "attempting to add property '".. desc.name .."' with duplicate property bit '".. desc.property_bit .."'!" ) property_bits_in_use[ desc.property_bit ] = true end -- validate property_switch if desc.property_switch then for _,p in ipairs(desc.property_switch) do assert(type(p) == "number", "invalid property switch") if p >= 0 then assert( not property_bits_in_use[ p ], "attempting to add property '".. desc.name .."' with duplicate property bit '".. p .."'!" ) property_bits_in_use[ p ] = true end end assert(desc.enum_values == nil or #desc.property_switch == #desc.enum_values, "'property_switch' has invalid number of elements") end -- validate type of 'default' (must match with type) if desc.type == "bool" then assert(type(desc.default) == "boolean", "invalid default for type 'bool'") end if desc.type == "float" then assert(type(desc.default) == "number", "invalid default for type 'float'") end if desc.type == "float2" then assert(type(desc.default) == "vector", "invalid default for type 'float2'") end if desc.type == "float3" then assert(type(desc.default) == "vector", "invalid default for type 'float3'") end if desc.type == "float4" then assert(type(desc.default) == "vector", "invalid default for type 'float4'") end if desc.type == "int" then assert(type(desc.default) == "number", "invalid default for type 'int'") end if desc.type == "texture2d" then assert(type(desc.default) == "table", "invalid default for type 'texture2d'") end if desc.type == "property_array" then assert(type(desc.default) == "table", "invalid default for type 'property_array'") end if desc.type == "material" then assert(type(desc.default) == "table", "invalid default for type 'material'") end -- validate editor_type local editor_types = utils.make_set{ "default", "slider", "color_rgb", "color_srgb", "color_rgba", "color_srgba" } desc.editor_type = desc.editor_type or "default" assert( editor_types[desc.editor_type], "invalid value for 'editor_type' (default or color_srgb expected)" ) -- debug dump -- for k,v in pairs(desc) do -- print(k, v) -- end -- allow property references from technique definitions _G[desc.name] = desc; if desc.type == "enum" then for i = 1, #desc.enum_values do _G[desc.name][desc.enum_values[i]] = desc.enum_values[i] end end if not constructing_property_array then add_property(desc) else add_array_property(desc) end end function material_properties.deprecated_material_properties() property{ name = "Skinning", type = "enum", default = "off", enum_values = { "off", "one", "four", "eight" }, property_switch = { -1, 27, 30, 23 }, serialized = false, } end function material_properties.common_material_properties() property{ name = "CastShadow", type = "bool", default = true, } property{ name = "CullMode", type = "enum", default = "ccw", enum_values = { "none", "cw", "ccw" }, } property{ name = "BlendMode", type = "enum", default = "none", enum_values = { "none", "alpha" }, } deprecated_material_properties() end -- defaults for textures function material_properties.default_texture() return { guid = "01c27045-b3a1-46f3-bc1f-dfe3f8fd7628", file_path = "sourcedata\\textures\\default.rmdj" } end function material_properties.default_white_texture() return { guid = "15312fe9-e653-4ad6-88b4-b383581df73a", file_path = "sourcedata\\textures\\defaultwhite.rmdj" } end function material_properties.default_black_texture() return { guid = "7bbe3e9f-8135-4723-b824-ee68ad98e34e", file_path = "sourcedata\\textures\\defaultblack.rmdj" } end function material_properties.default_normal_texture() return { guid = "86153ff8-739d-48dd-a703-6a9e85461f1b", file_path = "sourcedata\\textures\\default_n.rmdj" } end function material_properties.default_flat_normal_texture() return { guid = "713ccb29-3666-44a8-9dc4-09085f38bee7", file_path = "sourcedata\\textures\\default_flat_n.rmdj" } end function material_properties.default_diffuse_texture() return { guid = "b9c79d5c-a702-4a60-b2d3-340538186257", file_path = "sourcedata\\textures\\default_d.rmdj" } end function material_properties.default_specular_texture() return { guid = "5dac7665-af16-4ef3-950f-f44d61861ac0", file_path = "sourcedata\\textures\\default_sa.rmdj" } end function material_properties.default_smoothness_texture() return { guid = "9eb29db1-048a-4b15-b803-f94bc9fea02e", file_path = "sourcedata\\textures\\default_s.rmdj" } end function material_properties.default_height_texture() return { guid = "7bbe3e9f-8135-4723-b824-ee68ad98e34e", file_path = "sourcedata\\textures\\defaultblack.rmdj" } end function material_properties.default_detail_texture() return { guid = "d7269d31-432a-474c-a8ce-924763267382", file_path = "sourcedata\\textures\\detail\\default_flat_dt.rmdj" } end function material_properties.default_material() return { guid = "1a71eb0f-d953-43fe-a804-98ca232295da", file_path = "sourcedata\\materials\\default.material" } end -- serialization function material_properties.serialize_material_to_string(material) local serpent = require "serpent" return serpent.dump(material, { nocode = true }) end function material_properties.deserialize_string_to_material(material_as_string) local serpent = require "serpent" local _, copy = serpent.load(material_as_string, { safe = false }) return copy end -- technique definitions local technique_names = {} technique_names[""] = true function material_properties.technique(desc) local field_types = { name = "string", vertex_shader = "string", amplification_shader = "string", mesh_shader = "string", pixel_shader = "string", compute_shader = "string", raytracing_shaders = "table", vertex_properties = "table", amplification_properties = "table", amplification_enabled = "table", mesh_properties = "table", pixel_properties = "table", compute_properties = "table", raytracing_properties = "table", required_features = "table", enable_re_z = "table", fixed_properties = "table", } -- override assert and error so that any errors are propagated to the material script local error = function(msg) error(msg, 3) end local assert = function(cond, msg) if not cond then return error(msg) end end -- validate technique names assert(desc.name, "missing field 'name'") assert(not technique_names[desc.name], "attempting to add technique with duplicate name '".. desc.name .."'!") technique_names[desc.name] = true -- validate field names for name, value in pairs(desc) do if field_types[name] == nil then local supported_fields = "" for field, _ in pairs(field_types) do supported_fields = supported_fields .. field .. ", " end error("invalid technique field '" .. name .."'.\n\nSupported fields: " .. supported_fields) end end -- validate field types for field, expected_type in pairs(field_types) do if desc[field] and type(desc[field]) ~= expected_type then error("invalid type for '" .. field .."' (" .. expected_type .. " expected)") end end -- replace property references with names if desc.vertex_properties then for i = 1, #desc.vertex_properties do assert(type(desc.vertex_properties[i]) == "table", "vertex_properties: invalid property reference") desc.vertex_properties[i] = desc.vertex_properties[i].name end end if desc.amplification_properties then for i = 1, #desc.amplification_properties do assert(type(desc.amplification_properties[i]) == "table", "amplification_properties: invalid property reference") desc.amplification_properties[i] = desc.amplification_properties[i].name end end if desc.amplification_enabled then for i = 1, #desc.amplification_enabled do assert(type(desc.amplification_enabled[i]) == "table", "amplification_enabled: invalid property reference") desc.amplification_enabled[i] = desc.amplification_enabled[i].name end end if desc.mesh_properties then for i = 1, #desc.mesh_properties do assert(type(desc.mesh_properties[i]) == "table", "mesh_properties: invalid property reference") desc.mesh_properties[i] = desc.mesh_properties[i].name end end if desc.pixel_properties then for i = 1, #desc.pixel_properties do assert(type(desc.pixel_properties[i]) == "table", "pixel_properties: invalid property reference") desc.pixel_properties[i] = desc.pixel_properties[i].name end end if desc.compute_properties then for i = 1, #desc.compute_properties do assert(type(desc.compute_properties[i]) == "table", "compute_properties: invalid property reference") desc.compute_properties[i] = desc.compute_properties[i].name end end if desc.raytracing_properties then for i = 1, #desc.raytracing_properties do assert(type(desc.raytracing_properties[i]) == "table", "raytracing_properties: invalid property reference") desc.raytracing_properties[i] = desc.raytracing_properties[i].name end end if desc.enable_re_z then for i = 1, #desc.enable_re_z do assert(type(desc.enable_re_z[i]) == "table", "enable_re_z: invalid property reference") desc.enable_re_z[i] = desc.enable_re_z[i].name end end if desc.fixed_properties then for key, value in pairs(desc.fixed_properties) do assert(type(_G[key]) ~= nil, "fixed_properties: invalid property reference") assert(type(value) == "string" or type(value) == "boolean", "fixed_properties: invalid property value") end end add_technique(desc) end -- inject everything from module into global environment for k, v in pairs(material_properties) do _G[k] = v end return material_properties