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
samplersagainst 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_assetsanswers "what exists". Pair it withget_referencersfor "what is used". The gap between the two numbers is the finding.get_referencerstakespath, notassetPath. - A duplicate name across folders is the signature. Two materials called
M_Rustwith 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_expressionsgives each wire as{input, from, fromOutput}with no index. An unnamed input readsNone(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 asXYZand a texture sample asRGB.
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_parameterswithtree: trueloads only the firsttopinstances, 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
RefractionDepthBiasoverride, 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: truedoes. - 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_parametersleaves masks out when any level of the chain is cooked, setscomponentMasksReadto false and names the level incomponentMasksNote. - Never set an instance's editor-only data to
None.set_propertywithEditorOnlyData: "None"on an uncooked instance succeeds, and the next unfilteredlist_material_parameterstook 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.