Appopolis

Your Blender model exports to glTF as a white blob. You probably don't need to bake it.

21 August 2026 · blender, 3d, gltf, web

You build a material in Blender. Noise into the base colour, a Voronoi for the aggregate, a bump for the grain. It looks right in the viewport, right in Cycles, right in EEVEE.

You export it to .glb, open it in three.js or <model-viewer>, and the whole model is white.

Not untextured-grey. Not black. Flat, featureless white — as if the material never existed.

Every answer you find says the same thing: bake your procedural textures to images. That answer is correct, and for a lot of cases it is also more work than you need to do.

What is actually happening

glTF is a transport format. It is deliberately not a shading language. A glTF material stores a handful of values — baseColorFactor, metallicFactor, roughnessFactor — and optionally an image texture for each. That is the whole vocabulary.

A Blender node graph is not in that vocabulary. So when the exporter walks your Principled BSDF and finds a Noise Texture plugged into Base Color, it cannot translate it, and it does the only thing it can: it leaves baseColorFactor out.

Here is the part that surprises people. It does not fall back to the value in the socket. It omits the field, and the glTF specification says an omitted baseColorFactor defaults to [1, 1, 1, 1].

White. Not a bug, not a broken export — the format's default, faithfully applied.

You can see it for yourself without any 3D software. A .glb is a binary header followed by a JSON chunk:

import struct, json
d = open('model.glb', 'rb').read()
n = struct.unpack('<I', d[12:16])[0]
gltf = json.loads(d[20:20 + n])
for m in gltf['materials']:
    print(m['name'], m.get('pbrMetallicRoughness', {}).get('baseColorFactor'))

If that prints None for your materials, nothing is wrong with your viewer. The colour was never in the file.

The fix that takes two minutes

If what you need out of the export is a flat colour per material — and for a lot of work, especially review models, prototypes and stylised assets, it is — you do not have to bake anything.

Unlink the procedural inputs just for the duration of the export. With the node disconnected, the exporter reads the socket's own value and writes it into baseColorFactor.

import bpy

# inputs whose node graphs glTF cannot carry
FLATTEN = ('Base Color', 'Roughness', 'Metallic', 'Normal',
           'Emission Color', 'Emission Strength', 'Alpha')

def unlink_procedural():
    saved = []
    for mat in bpy.data.materials:
        if not mat.use_nodes or not mat.node_tree:
            continue
        for node in mat.node_tree.nodes:
            if node.type != 'BSDF_PRINCIPLED':
                continue
            for name in FLATTEN:
                inp = node.inputs.get(name)
                if not inp or not inp.is_linked:
                    continue
                for link in list(inp.links):
                    saved.append((mat.node_tree, link.from_socket, inp))
                    mat.node_tree.links.remove(link)
    return saved

def relink(saved):
    for tree, from_socket, to_socket in saved:
        tree.links.new(from_socket, to_socket)

saved = unlink_procedural()
try:
    bpy.ops.export_scene.gltf(filepath='model.glb', export_format='GLB',
                              use_selection=True, export_apply=True)
finally:
    relink(saved)

The try/finally matters. If the export throws, you want your material graph back.

The one line that decides whether this works

This trick only works if the socket has a sensible value in it to begin with.

That sounds obvious. It is not, because Blender does not require you to set one. If you build a material in Python and only ever link a node into Base Color, the socket keeps whatever it was born with, and unlinking gives you that — usually a default grey, sometimes white. You have swapped one wrong colour for another.

So whenever you write a material procedurally, set the value and link the node:

bsdf.inputs['Base Color'].default_value = (*base_rgb, 1)   # the fallback
# ...then build the noise/voronoi graph and link it in

One line. It costs nothing in Cycles, because the link overrides it, and it is the entire difference between a correct export and a white one.

We know because we shipped the bug. Our first pass set the fallback on every material and the exports were fine. Then we wrote three new ground materials — asphalt, paving, turf — that built their colour entirely in nodes and never touched default_value. Every render was correct. Every export was white, and we spent a while looking at the viewer before looking at the material.

When you do have to bake

Be honest about the limits. Unlinking gives you one flat colour per material. Bake when:

where the texture carries the identity of the surface.

plastic on anything meant to look worn.

real texture maps.

Do not bake when:

and flat colour is enough.

every change.

Three smaller traps in the same area

Modifiers do not export unless you ask. export_apply=True evaluates the modifier stack. Without it your bevels, arrays and subdivisions are simply absent, and the model arrives faceted.

Emission needs a strength above 1 to read as emissive. glTF carries it through KHR_materials_emissive_strength, which not every importer honours. Check the extension list in the JSON chunk before assuming the viewer is wrong.

Instances share mesh data; check that the exporter kept it. Objects created with obj.copy() reference one mesh. Blender's exporter deduplicates them, so two hundred trees can be a few megabytes. If your file is enormous, something broke the sharing — usually a modifier applied per-instance.

The short version

gives you white anyway.

Inspect the JSON chunk first, every time. It tells you in five lines whether the colour left Blender at all — which decides whether you are debugging the export or the viewer.