DOCUMENTATION

Materials

Common mistakes with materials, material instances, textures and imported mesh materials in Unreal through MifBridge. Read it when a material renders gray, a shader count looks wrong, a wire lands on the wrong pin or an import leaves extra assets behind.

A material that fails to compile turns gray, and the obvious call does not tell you

A material that fails to compile does not stop rendering. It renders as the engine's default material, which is flat gray. When a whole landscape or mesh goes gray, the cause is in the material's compile errors, not in anything you are looking at.

recompile_material can answer ok:true, recompiled:true with every shader-queue counter at zero while the shader map is still incomplete. The call that tells you is material_statistics with compile: true. It returns shaderMapComplete and a compileErrors array. A false in shaderMapComplete explains "my material does nothing" faster than any screenshot. Ask for the errors. Do not infer from the picture.

A related timing trap: right after a recompile, a material's shader map can be incomplete while nothing is compiling, because the editor compiles a new map's shaders on demand as something draws with them. A render started then draws an older map. recompile_material with wait: true asks for the rest and waits on the resource, up to its time limit. A first compile of a three-constant opaque material took 1.2 s on 5.3.2 and 2.9 s on 5.8.2.

Triplanar functions need the right sampler type

The engine's WorldAlignedTexture and WorldAlignedNormal material functions (under /Engine/Functions/Engine_MaterialFunctions01/Texturing/) fix textures that smear on steep faces. They take a TextureObject and a TextureSize. To keep the tiling of a WorldPosition * k UV scale, use TextureSize = 1/k: a scale of 0.0003 is one tile every 3333 uu.

A new MaterialExpressionTextureObject defaults to SAMPLERTYPE_Color, and the functions demand a sampler type that matches what you feed them (measured on 5.8):

What you plug in Required SamplerType Error if left at Color
base color SAMPLERTYPE_Color none, it is the default
a normal map SAMPLERTYPE_Normal "(Function WorldAlignedNormal) (Node TextureSample) Sampler type is Color, should be Normal"
roughness or any grayscale SAMPLERTYPE_LinearGrayscale "(Function WorldAlignedTexture) (Node TextureSample) Sampler type is Color, should be Linear Grayscale"

Because the failure shows as a gray material, check material_statistics with compile: true after every change like this.

Shader instruction counts compare a material to itself

material_statistics returns vertexShaderInstructions, pixelShaderInstructions and samplers, the figures the material editor's Stats panel shows. They are the maximum across representative shaders for this editor's feature level, not a sum over permutations.

A lower pixel count does not make one material cheaper than another. The number ignores how many permutations compile, how many pixels the material covers and overdraw. A 150-instruction translucent material drawn over the whole screen four times costs far more than a 380-instruction opaque one on a doorknob.

Honest uses:

  • Compare a material to itself across a change.
  • Spot a material that has quietly grown.
  • Read samplers against the platform cap (16 on most targets). A landscape material with layer blends is where that cap is reached.

FBX imports

Imports leave orphaned mesh-local materials

Importing an FBX creates materials next to the mesh. If the project later authors its own materials in a materials folder and reassigns the meshes, the import-time copies stay: same names, a different folder, zero referencers. In one project, 25 of 75 materials were such orphans, exactly a third, and every one had zero referencers.

They cost nothing at runtime, but they are cooked, they pad the registry and they make every count of the project's materials wrong. Two rules follow:

  • An asset count is not a usage count. find_assets answers "what exists". Pair it with get_referencers for "what is used". The gap between the two numbers is the finding. get_referencers takes path, not assetPath.
  • A duplicate name across folders is the signature. Two materials called M_Rust with different instruction counts are one authored material and one import leftover, and the cheap-looking one is usually the leftover, because it was generated from the FBX with a base color and nothing else.

An unused material gets no slot

An FBX mesh node carries every material from the Blender object's slots, but the static mesh Unreal builds has a slot only for materials its polygons use. A cube with four materials and faces on two came in with two slots on 5.8.2 (Interchange) and on 5.3.2 (the legacy importer). A check that holds the asset to the object's full material list refuses a valid mesh. Compare against the materials the written polygons use, which bl_export_mesh reports per piece as materialsUsed. See Blender to Unreal.

Wires and pins

A pin's name depends on who asks.

  • list_material_expressions gives each wire as {input, from, fromOutput} with no index. An unnamed input reads None (ComponentMask, OneMinus, Saturate, Abs, Frac and Sine). A plan's "" means the first output.
  • The editing library shortens pin names before matching: "Coordinates" is "UVs", "TextureObject" is "Tex", "Exponent" is "Exp", and "Input" becomes no name at all. A ComponentMask's or OneMinus's single input is reachable only as "". A function call is the exception: its inputs are compared unshortened.
  • Output names moved at UE 5.7. A VectorParameter's outputs are unnamed masks on 5.3 to 5.6 and "RGB".."RGBA" from 5.7. "" (output 0) is the only spelling for its color on every engine.
  • A new ComponentMask has R and G both set. Clear what you do not want.
  • A one-channel source such as PixelDepth or SceneDepth reads as R, a WorldPosition as XYZ and a texture sample as RGB.

A stock material function's pins are the FunctionInput and FunctionOutput nodes in its package. An output with no stored name is Result, and an input with no stored type is Vector3. An input whose bUsePreviewValueAsDefault is false is required: left open and reached, it compiles to "Missing function input". For example, SimpleGrassWind requires AdditionalWPO, and ParallaxOcclusionMapping requires only its Heightmap Texture.

Landscape Grass Output nodes have their own trap, covered in Common mistakes with the world.

Material instances

  • The whole instance tree is readable without loading an instance. The parent is a registry tag on UE 5.3 to 5.8, written as export text such as /Script/Engine.Material'/Game/M.M', so it needs unwrapping. list_material_parameters with tree: true loads only the first top instances, because override values are not in the registry.
  • The instance editor adds an override you did not write. Every edit in the Material Instance Editor adds a RefractionDepthBias override, and from then on the instance stops following its parent's value. A report of what an author overrode should treat it as engine-written when it equals what the parent resolves. tree: true does.
  • Cooked instances lose static component masks. Masks are editor-only data and a cook strips them. On a cooked instance the editor-only data is an empty object, not null, and listing masks anyway answers wrongly. list_material_parameters leaves masks out when any level of the chain is cooked, sets componentMasksRead to false and names the level in componentMasksNote.
  • Never set an instance's editor-only data to None. set_property with EditorOnlyData: "None" on an uncooked instance succeeds, and the next unfiltered list_material_parameters took a 5.8.2 editor down. See Losing work.

Textures

A texture's settings are readable from the asset registry without loading it, on some engines. CompressionSettings, SRGB, LODGroup and LODBias are registry tags on UE 5.3 to 5.8. MaxTextureSize and PowerOfTwoMode became tags in 5.5, so on 5.3 and 5.4 a cap set through Maximum Texture Size cannot be seen without loading the texture. Dimensions is the source size: a LOD bias, a maximum size or a texture-group cap is not applied to it. A loaded texture reports live values and an unloaded one what it was saved with.

The engine rewrites two settings on every edit: sRGB is forced off for Normalmap, Masks, Alpha and HDR compression, and MaxTextureSize is rounded up to a power of two. set_texture_settings refuses a maximum size that is not a power of two instead of writing a number the engine will not keep.

Breaking up texture tiling by rotating UVs

The common recipe for hiding the repeat of a large ground texture feeds a Voronoi texture's color into a Mapping node's Rotation, with that node driving every texture's vector input. The nodes voronoi, noise and mapping are all available through add_material_expression. Two details are usually wrong, and both look fine in a flat preview. This section follows from what the nodes compute and what a normal map stores.

  • A tangent-space normal map cannot just have its UVs rotated. Color and roughness are fine with a rotated lookup. A normal map stores a vector in tangent space, so a rotated lookup returns the right texel with its X and Y still in the original frame, and every cell's surface detail is lit as if it had not been rotated. Counter-rotate the sampled normal's XY by the same angle before the Normal Map node. The error shows most under raking light, such as a torch or a low sun.
  • A Voronoi color into Rotation rotates about all three axes. Rotation is an Euler triple in radians. Plugging the three color channels in tips the UV plane out of its own surface on a ground plane. Take one channel, multiply by 2 pi, build (0, 0, Z) with Combine XYZ and feed that to Rotation.