3D Materials
Material3D is a struct that defines surface appearance for PBR rendering. It carries color, texture maps, and scalar properties, but never a shader handle. The pipeline binds the appropriate shader.
What and Why
Materials describe what a surface looks like. The pipeline's shader reads material properties to compute lighting. You set materials on meshes; the pipeline handles the rest.
Key properties:
- Albedo: Base color and optional texture (diffuse look)
- Roughness: How rough vs mirror-smooth (0 = mirror, 1 = fully diffuse)
- Metallic: Dielectric vs metallic surface (0 = plastic/wood, 1 = metal)
- Normal map: Surface detail without extra geometry (normal map)
- Emission: Self-illumination (glowing surfaces)
- Opacity: Transparency
Quick start
// Simple red material
let redMat = Material3D.colored Color.Red
// PBR metal with roughness (record update for scalar properties)
let metalMat = {
Material3D.defaults with
AlbedoColor = Color(180, 180, 180, 255)
Roughness = 0.2f
Metallic = 1.0f
}
// Textured material
let woodMat =
{ Material3D.defaults with Roughness = 0.8f }
|> Material3D.withAlbedoMap woodTexture
// In your view (MonoGame shown; raylib passes its own Mesh;
// prims comes from Primitive3D.create gd, see Primitive meshes below):
buffer
.mesh(prims.Cube, transform, metalMat)
.drop()
Material3D fields
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Base color, multiplied with albedo map |
|
|
|
Albedo/diffuse texture |
|
|
|
Perceptual roughness (0 = mirror, 1 = diffuse) |
|
|
|
Roughness texture (green channel) |
|
|
|
Metallic factor (0 = dielectric, 1 = metal) |
|
|
|
Metallic texture (blue channel) |
|
|
|
Normal map for surface detail |
|
|
|
Self-illumination color |
|
|
|
Emission texture |
|
|
|
Alpha (>= 1 = opaque, 0 = invisible, in between = transparent) |
|
|
|
UV tiling multiplier |
Builder pattern
All materials start from Material3D.defaults or a convenience constructor. Use record update syntax for scalar properties and with* functions for texture maps (as in the Quick start above). Shorthand constructors cover the common cases:
let red = Material3D.colored Color.Red // albedo = red, rest default
let glow = Material3D.unlit Color.Yellow // emissive, no lighting
Texture maps
Textures are optional. When absent, the scalar/color value applies. When present, the texture is multiplied with the scalar.
Map |
Purpose |
Typical source |
|---|---|---|
|
Base color / diffuse |
PNG/JPG color texture |
|
Per-pixel roughness |
Grayscale, green channel |
|
Per-pixel metallic |
Grayscale, blue channel |
|
Surface normals |
Tangent-space normal map |
|
Self-illumination |
Color texture |
let mat =
Material3D.defaults
|> Material3D.withAlbedoMap albedoTexture
|> Material3D.withNormalMap normalTexture
|> Material3D.withRoughnessMap roughnessTexture
|> Material3D.withMetallicMap metallicTexture
Loading from model files
When you load a .obj, .gltf, or .fbx via the asset system, the backend's native material is
extracted into a Material3D automatically. If you don't need per-mesh control, .model(...)
does this for you; the loops below show what it does internally (the conversion helper differs
by backend because the native material type differs):
// raylib: read a raylib Material
let m = assets.Model("assets/mymodel.obj")
for i = 0 to m.MeshCount - 1 do
let mesh = NativePtr.get m.Meshes i
let matIdx = NativePtr.get m.MeshMaterial i
let raylibMat = NativePtr.get m.Materials matIdx
let mat = Material3D.fromRaylibMaterial raylibMat
// MonoGame: read a ModelMeshPart's native Effect (BasicEffect/SkinnedEffect)
let model = assets.Model("assets/mymodel")
for mesh in model.Meshes do
for part in mesh.MeshParts do
let mat = Material3D.fromModelMeshPart part
In both cases the .model(...) member does this conversion automatically for all
sub-meshes; use it when you don't need per-mesh control. On MonoGame only the albedo color,
albedo map, and opacity are extracted from native effects (normal/roughness/metallic maps are
not carried by MonoGame's standard effects); assign the remaining PBR maps explicitly if needed.
Unlit materials
Material3D.unlit creates an emissive material that ignores lighting:
let glow = Material3D.unlit Color.Cyan
Use for UI elements, debug markers, or anything that should appear at full brightness regardless of scene lighting.
Transparency
Opacity controls how the surface mixes with what is behind it:
Opacity >= 1.0: opaque. Renders in the forward pass, writes depth, casts shadows.-
0 < Opacity < 1.0: transparent. Renders after all opaque geometry, sorted far-to-near by camera distance, and alpha-blends with the scene. Depth testing stays on but depth writes are off for the sorted pass, so a transparent surface never occludes the geometry behind it in the depth buffer. Opacity <= 0.0: not rendered at all (no draw, no shadow).
let glassMat = {
Material3D.defaults with
AlbedoColor = Color(200, 220, 255, 120) // alpha 120/255
Opacity = 120.0f / 255.0f
Roughness = 0.05f
Metallic = 0.9f
}
buffer.mesh(prims.Cube, transform, glassMat).drop()
Rules:
-
Transparent geometry does not cast shadows and does not write depth. The shadow and
scene-depth passes are binary, so they cannot represent partial occlusion; transparent
surfaces are excluded from both, and the sorted pass writes with depth off. Consequence:
PostProcessWithDeptheffects (fog, depth-of-field) sample opaque-only depth on both backends.EnableShadows/DisableShadowsscopes do not change this. - Ordering is per-camera, far-to-near. Transparent draws sort by distance to the camera that captured them and render at camera boundaries and end of frame. Intersecting transparent surfaces can still sort incorrectly; there is no per-pixel ordering.
-
Where opacity comes from. On raylib,
Opacitymaps from the albedo texture/color alpha channel (Color.A); on MonoGame, from the effect's alpha (BasicEffect.Alpha/SkinnedEffect.Alpha). On both backends the sorted transparent pass uses alpha blending with depth writes off (depth test on) for its duration. -
Limitations. Instanced draws and draws inside a
beginEffect/endEffectscope are not deferred in this version: they render immediately and unsorted. On raylib such a transparent surface still blends (with depth writes on, since it skips the sorted flush); on MonoGame it renders effectively opaque, because the frame'sBlendState.Opaqueis not switched for these immediate draws. Either way a transparent instanced or effect-scoped surface may look wrong against sorted transparents; prefer fully opaque materials for them.
Primitive meshes
The backend provides primitive meshes for basic shapes. The mesh type differs by
backend (raylib Mesh / MonoGame PrimitiveMesh), and .mesh(...) takes whichever yours has:
Shape |
raylib |
MonoGame |
|---|---|---|
Unit sphere |
|
|
Unit cube |
|
|
Unit cylinder |
|
|
Unit plane |
|
|
Torus |
|
|
Unit cone |
|
|
// raylib:
let transform = Matrix4x4.CreateScale(2f, 1f, 3f) * Matrix4x4.CreateTranslation(pos)
buffer.mesh(Primitive3D.cube, transform, mat).drop()
// MonoGame: build the primitive set once (needs the GraphicsDevice), then draw
let prims = Primitive3D.create gd
let transform = Matrix.CreateScale(2f, 1f, 3f) * Matrix.CreateTranslation(pos)
buffer.mesh(prims.Cube, transform, mat).drop()
_IMPORTANT_: Use
.mesh(...)with these primitives instead of backend-native immediate draws (e.g.Raylib.DrawCube). Direct native draws bypass the pipeline's shader and won't receive PBR lighting or shadows.
Overriding a model's material
.model(...) always renders with the material baked into the file (auto-extracted per
sub-mesh, as described above). When you want a different material (the authored values look
wrong, you want to reuse one mesh for several looks, gold/silver/bronze variants of the same
model, or you need a flat/debug material), use the override members instead of iterating the
model's meshes by hand:
// Whole-model override: every sub-mesh uses the supplied material
buffer.modelWith(model, transform, Material3D.colored Color.Gold).drop()
// Per-sub-mesh override: a resolver returns the material for each sub-mesh
let goldResolver (i: int) =
if i = 0 then Material3D.colored Color.Gold else Material3D.defaults
buffer
.modelWithPerMesh(model, transform, goldResolver)
.drop()
The override goes through the normal PBR and shadow path, so it is lit and shadowed like
an authored material. The default .model(...) path is unchanged; overriding is opt-in
and costs nothing when you don't use it.
Resolver index. The int -> Material3D resolver is indexed by the pipeline's sub-mesh
iteration order, which differs by backend because the native model types differ:
- raylib: mesh index
0..model.MeshCount-1. - MonoGame: a flat counter over
model.Meshes × MeshParts.
Write the resolver against your specific model's structure; it is not portable across backends.
Animated models. MonoGame's animated draw carries no material (it auto-extracts per part,
like .model(...)), so the same override pair exists for it:
buffer.animatedModelWith(animatedModel, transform, material).drop()
buffer.animatedModelWithPerMesh(animatedModel, transform, resolver).drop()
On raylib, explicit-palette skinned draws go through .skinnedMesh(...), which already takes a
Material3D directly; supply the material you want there; no separate override helper is
needed.
See also
- Overview: Architecture and pipeline setup
- Draw DSL: The fluent draw surface
- Lighting: Light types and shadow configuration
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
Mibo