Shape and surface are different data#
A mesh describes where a surface exists. A material describes how that surface interacts with light. Reusing a mesh with several materials avoids copying geometry. Reusing a material across many instances avoids rebuilding identical bindings and pipelines. QubicEngine’s MeshAsset contains vertex streams, index data, submesh ranges, local bounds, and material slots. A MaterialAsset contains a shader family, feature flags, scalar parameters, and texture handles. Render extraction combines them with an instance transform to create a draw packet.
The vertex contract#
Position places a vertex. A normal describes a surface direction for lighting. UV coordinates address a texture. A tangent defines a local direction used with the normal and handedness to interpret a normal map. An index references a vertex so connected triangles can share data. A hard edge or UV seam often needs duplicated vertices because one position can require different normals or UVs on each side. Submeshes identify index ranges with different materials. Model import must validate indices, normalize influences, convert coordinate conventions once, and compute usable bounds.
Light meets a surface#
Light meets a surface This illustration uses canvas. The explanation below describes the same process.
The browser playground uses a named Blinn–Phong approximation to make the controls legible. The reference renderer uses an energy-aware microfacet model with GGX distribution. Both demonstrate roughness spreading a specular highlight and metalness changing the relationship between diffuse and specular reflection.
| Channel | Meaning | Sampling convention |
|---|---|---|
| Base color | Diffuse color or metal reflectance | sRGB decoded to linear |
| Roughness | Spread of microscopic surface directions | Linear scalar |
| Metalness | Dielectric-to-metal material parameter | Linear scalar |
| Normal | Local surface perturbation | Linear vector data |
| Emission | Light contributed by the surface | Authored color convention, then linear HDR |
| Opacity | Coverage or transparency | Linear scalar |
Implementation: a draw packet
struct MaterialGpu {
float baseColor[4];
float roughness;
float metalness;
uint32_t baseColorDescriptor;
uint32_t normalDescriptor;
};
struct DrawPacket {
BufferHandle vertices;
BufferHandle indices;
uint32_t firstIndex;
uint32_t indexCount;
uint32_t materialIndex;
uint32_t instanceIndex;
};BufferHandle is an opaque generation-checked graphics resource handle. The baseline binding scheme uses a per-frame constant buffer at b0, a material buffer at t1, textures beginning at t0 in register space1 in their own descriptor range, and a sampler at s0. The introductory sample simplifies this to sixteen root constants at b0, one SRV at t0, and one static sampler at s0. Binding schemes are explicit at each transition rather than silently interchangeable.
Shader variants and pipeline caching#
A shader family defines compatible vertex layouts and bindings. Feature flags choose code variants such as skinning or alpha testing. The compiler key includes source content, include dependencies, defines, entry point, shader model, and backend target. A pipeline key adds root signature or pipeline layout, vertex format, attachment formats, depth/raster state, and shader hashes. Compile or warm expected variants outside the critical frame path. A new parameter value usually belongs in material data; it does not require a new pipeline. Too many independent boolean features multiply permutations. Keep the material model expressive without generating combinations that no asset uses.
Costs and failure modes#
Instancing shares mesh and material state while varying transform data. Sorting opaque draw packets reduces state changes, although front-to-back ordering can reduce overdraw. Those goals can compete. Measure whether submission or shading dominates before optimizing one. Broken normal maps often come from a missing tangent, incorrect handedness, a flipped green channel, or data sampled as sRGB. A black metallic surface can simply lack reflected environment light. Texture swimming can indicate bad UVs or a mismatch between vertex stride and input layout. Use a debug view of normals and UVs before changing lighting equations. Read mesh import, shaders and lighting, and the rendering pipeline.