Skip to content

t1k:cocos:base:effect-authoring

FieldValue
Modulebase
Version1.16.0
Effortmedium
Tools

Keywords: cceffect, ccprogram, chunk, cocos, cocos-creator, effect, glsl, include, macro, material, mtl, shader, uniform

/t1k:cocos:base:effect-authoring

Hand-authoring .effect files for Cocos Creator 3.8.7. Covers the whole chain from GLSL to a material visible on a node: pick the archetype, get the chunk includes right, pack properties, author the .mtl, wire it up, verify it compiled.

The expensive part of Cocos shader work is not writing GLSL — it is the include graph and the fact that a failed import reports EFX2406: compilation failed: EXPAND THIS MESSAGE FOR MORE INFO and nothing else. This skill exists to make the first attempt compile, and to make the second attempt diagnosable.

Version scope: claims below are verified against 3.8.7 only. § 3 includes the procedure to re-verify on another version — run it rather than assuming.

  • Writing a new .effect from scratch, or porting one from another engine (pair with t1k-cocos-base-unity-shader-port).
  • An .effect fails to import and the editor error is unreadable.
  • A material exists but does not show up on the node, or MCP refuses to assign it.
  • Reviewing an existing .effect for the anti-patterns in § 7.
QuestionYes →No →
Rendered under a Canvas (Sprite / Label / UI)?B. UI spritereferences/archetype-ui-sprite.mdnext
Time-scrolled, additive, no depth (background flow, glow, energy)?C. Scroll-additive VFXreferences/archetype-scroll-vfx.mdnext
Needs world position or world normal (fresnel, rim, world-space FX)?A. 3D unlit meshreferences/archetype-3d-unlit-mesh.mdA, minus the world-matrix includes

Archetype D — imported from DCC (a 400+ line multi-technique surface shader, e.g. imported-specular-glossiness-fade.effect): do not hand-author or hand-edit these. Change the material’s property values; if the shading model itself is wrong, re-export from the DCC tool.

CCEffect %{ ...YAML... }% ← techniques / passes / render state / properties
CCProgram <name> %{ ...GLSL... }% ← one block per shader stage, referenced as `<name>:<fn>`

A pass wires them: vert: my-vs:vert means “program my-vs, entry function vert”.

The single most important behavioral fact: the editor compiles every macro variant, not just the one your material uses. A #if branch that never runs at runtime still has to compile. This is why a missing include breaks the import even for a mesh that is never instanced — see § 3.

Each row is verified against engine chunk source at <CocosInstall>/resources/resources/3d/engine/editor/assets/chunks.

You needIncludeEvidenceTrap
World matrix (pos/normal)legacy/decode-base and legacy/local-batchdecode-base.chunk:13-16 declares the attributes; :32-38 declares a_matWorld0/1/2see “world-matrix trap” below
Skinned meshlegacy/input-standard (instead of decode-base alone)input-standard.chunk:12-21CCVertInput calls CCSkin(position, normal, tangent)An unskinned normal freezes a fresnel rim in bind pose while the mesh animates under it
Sprite texture (2D/UI)#pragma builtin(local) + layout(set = 2, binding = 12) uniform sampler2D cc_spriteTexture;all three builtin 2D effects use binding = 12do not also include decode-base — it declares a_position, and your own declaration then collides
Alpha testbuiltin/internal/alpha-test, then call ALPHA_TEST(color)alpha-test.chunk:9-19, two overloads (vec4, float)the uniform block only exists under USE_ALPHA_TEST; calling it unguarded is fine, the macro handles it
Timebuiltin/uniforms/cc-globalcc_time.x
Local world matrix without instancingbuiltin/uniforms/cc-localcc_matWorldguard behind #if USE_LOCAL for 2D

The world-matrix trap — why two includes, not one

Section titled “The world-matrix trap — why two includes, not one”

legacy/local-batch.chunk is two lines:

#include <builtin/uniforms/cc-local-batched>
#include <builtin/functionalities/world-transform>

It declares no attributes. CCGetWorldMatrix / CCGetWorldMatrixFull actually live in world-transform.chunk, and their #if USE_INSTANCING branch reads a_matWorld0/1/2 — which only decode-base declares, and only inside its own USE_INSTANCING guard.

Because the editor compiles every variant, the USE_INSTANCING=1 variant is always built. So including world-transform (or local-batch) without decode-base fails every time, even for a mesh that is never instanced:

Error EFX2406: compilation failed: ↓↓↓↓↓ EXPAND THIS MESSAGE FOR MORE INFO ↓↓↓↓↓
ERROR: 0:21: 'a_matWorld0' : undeclared identifier

Two dead ends worth naming:

  • builtin/uniforms/cc-local-batch does not exist. The chunk is cc-local-batched (past tense). Wrong name → Error EFX2001: can not resolve 'builtin/uniforms/cc-local-batch'.
  • input-standard reaches decode-base indirectly — via decode-standard (decode-standard.chunk:2). So skinned meshes get the attributes for free and must NOT include decode-base again.

Re-verifying on a version other than 3.8.7

Section titled “Re-verifying on a version other than 3.8.7”
Terminal window
CHUNKS="<CocosInstall>/resources/resources/3d/engine/editor/assets/chunks"
ls "$CHUNKS/builtin/uniforms/" | grep local # confirm exact chunk names
cat "$CHUNKS/legacy/local-batch.chunk" # see what it actually pulls in

Then copy the include block from that version’s own assets/effects/builtin-unlit.effect. Grep before you guess — a wrong chunk name costs a full import cycle.

Full reference: references/properties-and-macros.md. The conventions that matter:

  • Pack scalars into a vec4, don’t give each float its own uniform:
    emissionPower: { value: 1.0, target: params.x, editor: { displayName: Emission Power } }
    fresnelPower: { value: 0.0, target: params.y, editor: { slide: true, range: [0.0, 10.0], step: 0.01 } }
    uniform Constants { vec4 mainColor; vec4 params; };
  • Name the uniform block Constants.
  • editor: { parent: USE_XXX } hides a property until its macro is on; visible: false hides the raw packed vector.
  • Declare an optional feature as a macro and gate both the sampler and its use:
    #if USE_EMISSIVE_MAP
    uniform sampler2D emissiveTexture;
    #endif

A .effect that “looks fine” and a .effect that imported are different things. Always run this.

Step 1 — force the import. A running editor does not pick up newly written asset files on its own:

Terminal window
curl -s -X POST http://127.0.0.1:3000/mcp \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"manage_asset","arguments":{"action":"refresh","url":"db://assets/<your-folder>"}}}'

No MCP? Use the editor’s Assets → Refresh. Never hand-write the .effect.meta — let the editor generate it.

Step 2 — check the artifact. A successful import writes library/<first 2 chars of uuid>/<uuid>.json:

Terminal window
UUID=$(grep -o '"uuid": "[^"]*"' path/to/my.effect.meta | head -1 | cut -d'"' -f4)
ls "library/${UUID:0:2}/$UUID.json" && echo COMPILED || echo FAILED

No artifact = compile failed. There is no other reliable signal.

Step 3 — read the real error. The editor UI truncates to EXPAND THIS MESSAGE FOR MORE INFO; the full GLSL error is in the log (which contains binary bytes, hence grep -a):

Terminal window
grep -a "my.effect" temp/logs/project.log | tail -20

Decode what you find with references/troubleshooting.md.

Full detail: references/material-and-wiring.md.

.mtl is plain JSON referencing the effect by uuid. Colors are integers 0–255, not floats 0–1:

{
"__type__": "cc.Material",
"_effectAsset": { "__uuid__": "<effect uuid>", "__expectedType__": "cc.EffectAsset" },
"_techIdx": 0,
"_defines": [ { "USE_EMISSIVE_MAP": true } ],
"_props": [ { "mainColor": { "__type__": "cc.Color", "r": 255, "g": 242, "b": 242, "a": 255 } } ]
}

MCP cannot assign a MeshRenderer material array. set_property on sharedMaterials / _materials silently no-ops (changeVerified: false), passing an array value hangs the editor for ~2 minutes then times out, and the indexed path sharedMaterials.0 is rejected outright. Mesh assignment (property: "mesh") works normally.

Workaround: hand-author a small .prefab with _materials baked in, refresh, then manage_node create assetUuid:<prefab>. Recipe in references/material-and-wiring.md.

Don’tWhy
Guess a chunk namecc-local-batch vs cc-local-batched costs a full import cycle. Grep the chunks folder.
Give every float its own uniformWastes uniform slots and makes the material editor a wall of fields. Pack into vec4.
Hand-write .effect.metaThe editor owns the uuid. Write the .effect, refresh, let the meta appear.
Declare in vec3 a_position; after including decode-baseERROR: 0:154: 'a_position' : redefinition
Edit one effect of a shared-math pairEffects that port the same source shader (e.g. a body and a prop variant) drift apart silently. Note the pairing in a header comment and change both.
Treat “it compiled” as “it works”The binding number on a builtin-tagged sampler is not compile-validated — see references/archetype-ui-sprite.md.
FileContents
references/archetype-3d-unlit-mesh.mdVerified skeleton, fresnel/rim math, skinning, instancing
references/archetype-ui-sprite.mdVerified skeleton, cc_spriteTexture binding caveat, 2D macros
references/archetype-scroll-vfx.mdVerified skeleton, additive state, UV scroll + noise distortion
references/properties-and-macros.mdFull editor: field table, packing rules, #pragma define-meta
references/material-and-wiring.md.mtl schema, MCP limits, bake-prefab workaround
references/troubleshooting.mdError code → cause → fix, with verbatim messages
  • t1k-cocos-base-unity-shader-port — porting from a Unity .shader. It produces a port spec (archetype, property table, render state, translated fragment math) and hands off here for the actual file. Start there when the source is Unity; start here when authoring fresh.
  • t1k-cocos-playable-unity-particle — Unity ParticleSystem prefab conversion. Its materials still need an .effect; this skill covers that half.