## RDPQ Materials Last update: July 29, 2025 Materials are a way to define, encode and apply a group of render settings that can define a material in a scene. They answer to the question: "how can I define the properties for rendering this material in data rather than in code?" The pipeline works as follows: * Materials are defined in textual INI files (`.mat`). * An alternative JSON representation is also available (`.jmat`) -- especially since it can be embedded in GLTF more easily than an INI. * Materials are built into binary files via the new `mkmaterial` tool. Building is designed to be easily integrated into standard Libdragon makefiles. The tool generates two outputs at the same time: * A `.mdb` file, that contain multiple materials in binary format. Alternatively, multiple single-material files can be output, though they're more inconvenient to use if left on the filesystem. * A texture database: a directory containing multiple `.sprite` files, that are indexed in the `.mdb`. `mkmaterial` spawns `mksprite` to convert texures, so there is no further separate step needed. * At runtime, the new `rdpq_mat` module allows to: * Open a `.mdb` material database * Access a material within it via name * "Activate" a material by applying the render settings to the RDP. ## `.mat` file format This is a simple example of a `.mat` file: ```ini [glass] tex0.name = grass1sq.rgba32.png tex0.fmt = RGBA32 tex0.s.repeats = inf tex0.t.repeats = inf tex0.s.mirror = true tex0.t.mirror = false combiner.rgb = tex0 - prim rm.antialias = none rm.alpha_compare = 1 ``` Each mat file can contain one or multiple materials. Each material is defined via the INI section name (eg: `glass`). Each line contain a material property. Material properties are hierarchically grouped by using the dot-format in the key name. * `tex0` / `tex1`. These properties specify the textures being used by the material. In addition to the input file name and format, you can also specify most mksprite parameters here, including texparms like mirroring, clamping, etc. * `combiner`: these properties allow to configure the color combiner used by the material. The combiner can be inserted in a custom "shader language" that mimics an arithmetic expression, or as a 4-element tuple of combiner slot values (eg: `combiner.rgb.raw = (tex, prim, 1, 0)`). * `blender`: these properties allow to configure the blender used by the material. In this case, no shader language is available yet so the blender can only be configured as tuple of slot values. * `rm`: these properties specify the render mode overrides. The assumption is that there will be a global render mode that can be configured by the applicatiom before drawing the mesh, and then each material can temporarily override it. For instace, an application might activate antialiasing globally, but a single specific material might turn it off via `rm.antialias = none`. Internally, this is implemented via `rdpq_mode_push()`, `rdpq_mode_pop()`. This design allows for zero material bleeding which is essential for a sane engine architecture, while still allowing interesting effects. ## `.jmat` Materials are also supported in JSON format: ```json { "glass": { "tex0.name": "grass1sq.rgba32.png", "tex0.fmt": "RGBA32", "tex0.s.repeats": "inf", "tex0.t.repeats": "inf", "tex0.s.mirror": "true", "tex0.t.mirror": "false", "combiner.rgb": "tex0 - prim", "rm.antialias": "none", "rm.alpha_compare": "1" } } ``` JSON support is mostly useful for tooling generating these files from a different source (eg: from a 3D authoring program). Notice that for symmetry with the INI format, `.jmat` files are expected to have keys always containing strings. ## `mkmaterial` The `mkmaterial` tool is quite simple at the moment: ``` Usage: mkmaterial [flags] <file.mat>... Command-line flags: -v, --verbose verbose output -h, --help print this help message -I, --include [path] specify additional texture path -o, --output [path] specify output path (default: .) -t, --texdb [path] specify texture database path (default: {output}/texdb) -c, --compress [level] specify compression level for textures (default: 1) --raw-material generate a single raw headerless material instead of a database ``` `--include` specifies one or multiple directory where input PNG files can be found. `--output` is the path where `.mdb` files will be written. `mkmaterial` will generate one `.mdb` file per input `.mat` or `.jmat` file (which can contain multiple materials of course). `--texdb` is the output path where the texture database will be created. It will normally be within the `filesystem` directory. `--compression` is the compression level for texture files, which is basically forwarded to `mksprite` (mdb files are extremely small at the moment so they don't support compression). `--raw-material` is to support some experimentation with a different approach of encoding each material separately. In this case, the tool will output a single material blob (to be opened via `rdpq_mat_load_buf`) and a single-material `.mat`/`.jmat` file must be provided as input. ## Scenario 1: custom model format In this scenario we assume a 100% custom made pipeline to export meshes and materials from a 3D authoring tool of choice. * The user exports models from 3D tool into a format of choice (eg: GLTF). * The user writes materials by hand in `.mat` format. * The user uses a custom tool that converts GLTF into their own model format. Materials will be referred by name, with a name matching the one used in the `.mat` file. * The user also uses `mkmaterial` to create `.mdb` files, one per model (plus a shared global texture database for all models / scenes). * At runtime, every time a model is loaded, `rdpq_matdb_open` is called to open the corresponding `.mdb` for the model, obtaining a database pointer (`rdpq_matdb_t*`). * At model loading, for each material, `rdpq_matdb_load` is called to actually loading the material and its textures, obtaining a single material pointer (`rdpq_mat_t*`). * While drawing the model, `rdpq_mat_draw_begin()` and `rdpq_mat_draw_end()` are called around the draw calls, for each material. Filesystem for this example: ``` filesystem/ hero.mymodel hero.mdb enemy.mymodel enemy.mdb textures/ 0219aa5d.sprite 42c270ac.sprite 602e45d4.sprite f0a92214.sprite ``` ## Scenario 2: custom model format with fast64-rdpq In this scenario, we assume the user is willing to use its own model format, but will use Blender as authoring tool and there will be a new fast64 (called `fast64-rdpq`) that supports a rdpq-oriented GUI, preview and export. - The user authors the 3D models in Blender - The user also uses `fast64-rdpq` to create material in Blender, with an accurate preview via `f64render`. - The user exports models in GLTF format from Blender. These models will contain rdpq materials embedded in them in JSON format. - The user runs a custom tool to convert the GLTF to a custom model format. At this point, there are two ways to also convert materials into mdb files: - mkmaterial can be passed a gltf file as input to automtically extract JSON materials from it and create mdb files (NOTE: not implemented yet). - the tool can extract the material JSON from GLTF, spawn mkmaterial (which conveniently works also on stdin/stdout), and obtain a mdb file back. ## Tiny3D discussion In Tiny3d, models are in t3dm format and contain embedded material information, plus an external database format. If we want to keep exactly the same architecture, we need to implement a variation of the second scenario above: Assuming a fast64-rdpq exists: * `gltf_to_t3d` can extract JSON materials from GLTF, and call mkmaterial. Since t3dm uses an RIFF format with one chunk per material, the special option `mkmaterial --raw-material` must be used to obtain a single material blob. * Notice that `mkmaterial` also generates the texture database; it's important that it does so as texture parameters (mksprite flags) are embedded in the material. * At runtime, since there is no `mdb`, t3d can call `rdqp_mat_load_buf` to load a single material from a blob in memory. * During draw, `rdpq_mat_draw_begin` and `rdpq_mat_draw_end` can be used. This pipeline only works with a fast64-rdpq (that doesn't exist right now), and only with Blender. These are similar limitations to those that exist now. Alternative implementations: * Use `.mdb`. `.t3dm` only reference material by name or hash, and for each `.t3dm` there is one `.mdb`. This is basically scenario 1 above. `gltf_to_t3d` doesn't have to handle materials anymore at all, removing quite a bit of code. `mkmaterial` can work by itself over GLTF files. * Embed `.mdb` in a RIFF chunk. This requires writing code in `gltf_to_t3d` to call `mkmaterial` (without `--raw-material`), get a mdb file back for all the materials, and then embed that in the `.t3dm` chunk. At runtime, we would need a variant of `rdpq_matdb_open` that works from memory rather filesystem, but that's very easy to add. The only advantage is getting back to a single `.t3dm` file in the filesystem (assuming somebody cares about the filesystem layout in ROM), but textures would still be external like now. (Though in the future, we could grow a feature to embed textures in the mdb). ## Extension options It is common for users to need to add additional engine-specific properties in the material. For instance, Tiny3D has a few non-RDP related properties (eg: uvgen) that is currently storing in the material. In some cases, engine-related properties might be useful like IDs or physics-related attributes. While it is obviously possible for people to come up with a parallel data storage for such properties, it is very tempting to just add them into the material, and even edit them with a freeform property editor in authoring tools. Example: ```ini [glass] tex0.name = grass1sq.rgba32.png tex0.fmt = RGBA32 combiner.rgb = tex0 * prim ext.t3d.uvgen = SPHERE ``` * `mkmaterial` will parse all keys beginning with `ext.`. It will try to deduce the type from the value, so probably limited to `bool`, `int`, `float` and `string`. * The material will contain an optimized hash table of all the custom keys and their values. * At runtime, the API will allow to extract a certain extension key given its name and its type. For instance: `bool rdpq_mat_ext_bool(rdpq_mat_t* mat, const char *key, bool *value)`. The function will return `false` if the key was not defined in the material, allowing for rich defaults * With some preprocessor tricks, the above code can also avoid hashing a string at runtime. * There could also be an API to visit all extension keys/values couples in a material. This approach is quite flexible: users can just drop a new attribute in a `.mat` file (or in their authoring tool), rebuild, and extract it at runtime with literally one line of code. Obviously, this fully schema-less approach has also some cons: * There is a bit of runtime overhead in looking for a property. `rdpq_mat_ext_bool` will still have to search into a hashtable for the given key, and extract the value from there. Normally these attributes aren't really performance sensitive: they're queried once per frame per material, so it shoulnd't be noticable in CPU time. * There is no typo detection at build time. Typos will need to be checked at runtime, and require explicit code by the user enumerating all attributes and emitting warnings/errors on unknown ones. To determine the type of an extension value, mkmaterial applies the following logic: * If it's either `true` or `false`, it's a boolean. No other strings are interpreted as boolean. * If it's a sequence of digits, it's an integer (int32). Hex representaton is also supported, with the `0x` prefix. * If it's a sequence of digits with a period (`.`), it is a float. * Otherwise, the value is interpreted as a string. ## Other Future features * Allow reading a `.mdb` embedded into another file. For instance: `rdpq_matdb_open_buf`. * Add an option to `mkmaterial` to embed textures in `.mdb` rather than being in the external database. Together with the previous option, this would allow shipping single-file models that might be interesting in some scenarios. At runtime, there should be no changes to the API. * Add support for "material tracking", as in make sure that configuring a material only does the differences compared to the previous one. This would be an opt-in feature. It is surely useful to skip texture uploads at least. The balance in savings for standard properties need to be measured. * Add rdpq placeholder support. Somehow, there must be a way to mark a certain texture as being a rdpq placeholder that can be configured at runtime (eg: `tex0.name = placeholder:2`).