Add your own effects to Gold Motion
Gold Motion has a Custom category with a + button on it. Tap it, hand the app an XML file, and that effect shows up next to the built-in ones. No server, no account, no rebuild. The whole file is a header, a list of sliders, and a shader — and the header is the part everybody gets wrong.
What the Custom category actually does
Open the effects browser and you'll find a category called Custom. It ships with one entry and a + button above the list. Everything you add through that button lands here and behaves exactly like a stock effect — it goes on a layer, it has sliders, it animates, it deletes cleanly.
What happens when you tap +
Behind that one button the app does five things. Knowing the order saves you a lot of confusion later.
| Step | What the app does |
|---|---|
| 1 | Opens a file picker. You choose a .xml file. |
| 2 | Parses it with the same parser it uses for its own effects. If that fails you get “That XML is not a valid Gold Motion effect.” |
| 3 | Copies the file into the app's private custom_effects/ folder and writes a small .meta.json beside it. |
| 4 | Asks you for a thumbnail image. This is a required step, not an optional extra. |
| 5 | Writes its own content:// URI back into the header's thumb attribute. |
The app does not just read your header — it edits it. That single fact is the source of the one mistake nearly everybody makes, and it's covered in full in the next section.
The Custom + button does not exist in Gold Motion V6.x. This feature arrived in V7. If you are on an older build you will not find the button at all — update to V7.0.2 or newer before anything on this page applies.
The header, and the one rule that matters
The first tag in the file is the header. It names the effect, files it under Custom, and — critically — carries a
thumb attribute that must be present even before you have a thumbnail.
A correct header
<?xml version='1.0' encoding='UTF-8' ?>
<effect id="gm.custom.my_effect"
name="My Effect"
desc="One line describing what this does."
category="custom"
thumb="content://com.goldmotion.pnkj_com.alightcreative.app.motion.fileprovider/share/gmcustom/my_effect.png"
tags="gm,custom,color,glow">
Every attribute
| Attribute | What it does | |
|---|---|---|
| id | required | Unique name for the effect. It must start with gm.custom. — that prefix is how the app recognises it as a custom effect. Lowercase, dots and underscores only. |
| name | required | What you see in the list. This is also what the app checks for duplicates, so two effects with the same name will prompt you to replace. |
| desc | advised | Shown under the name. One or two lines. Long descriptions get clipped. |
| category | required | Must be custom. Anything else and the effect will not appear where you imported it. |
| thumb | required | The attribute the app refreshes. It must contain the pack's content:// file-provider line shown below so the selected image can be attached. |
| tags | optional | Comma-separated keywords. Start with gm,custom and add your own: glow,neon,text. |
| experimental | optional | true or false. Marks the effect as unstable. |
Why thumb is not optional
After you pick a thumbnail during import, the app looks through your XML for the literal text thumb="
so it can swap the value for its own file URI. If that text isn't in the file, there is nothing to swap — and the
thumbnail you just chose never gets attached to the effect.
The effect still imports. It just sits there with a blank tile, and people spend an hour assuming the image was the wrong size. It wasn't. The attribute was missing.
Every custom effect file must contain thumb="…" in the header — even if the value is a
placeholder you make up. If you remember one thing from this page, remember this one.
The exact thumb line to use
Use this line in the effect header. Change only the final my_effect.png when your slug changes; Gold Motion refreshes the URI after you select a thumbnail during import.
thumb="content://com.goldmotion.pnkj_com.alightcreative.app.motion.fileprovider/share/gmcustom/my_effect.png"
That URI points at the image copy managed by the app's file provider. Keep the complete attribute in the header so the import process can attach the picture you choose.
Copy this and only change three things
If you're starting from scratch, use this as your template. Change my_effect, the
name, and the desc. Leave everything else exactly as it is.
<?xml version='1.0' encoding='UTF-8' ?>
<effect id="gm.custom.my_effect"
name="My Effect"
desc="What it does."
category="custom"
thumb="content://com.goldmotion.pnkj_com.alightcreative.app.motion.fileprovider/share/gmcustom/my_effect.png"
tags="gm,custom">
<params>
<texture id="inputImg" srcType="content" />
<spinner id="amount" default="0.6" min="0.0" max="1.0" step="0.01" label="Amount" />
</params>
<shader type="fragment" precision="high"><![CDATA[
void main(){
vec2 uv = acScreenNorm;
vec4 src = texture2DCv(inputImg.texture, uv);
gl_FragColor = vec4(src.rgb * (1.0 + amount * 0.3), src.a);
}
]]></shader>
</effect>
The other header mistakes
| Mistake | Result |
|---|---|
id doesn't start with gm.custom. | The app won't treat it as a custom effect. |
Same name as an existing effect | Prompt: “<name> is already in your Custom effects.” |
Raw & or < inside a value | Malformed XML. The file is rejected outright. |
label="@am:string/…" copied from a stock effect | Labels render blank — those strings only exist inside the app. |
Missing the <?xml … ?> first line | Some parsers reject the file immediately. |
Unclosed <params> or <shader> | Not valid XML — same rejection as above. |
Importing an effect, step by step
This is the real order of operations, including the thumbnail prompt most guides forget to mention.
Get the .xml onto your phone
Copy the file anywhere you can browse to from a file picker — Downloads, Drive, Telegram, a folder in your gallery. There is no special folder to drop it in; the + button asks you to find it each time.
Open Gold Motion and select a layer
Any layer — photo, video, shape, text. An effect needs a layer to attach to, so have one selected before you go into the effects browser.
Open the effects browser and find Custom
Scroll the category list until you reach Custom. If you can't find it, you're on a build older than V7 — this category only exists from V7 onward.
Tap the + button
It sits at the top of the Custom category. That opens a document picker filtered to XML files.
Pick your .xml file
The app parses it straight away. If the header or the XML is broken you get “That XML is not a valid Gold Motion effect” and nothing is added — go back and check the header section above.
Pick a thumbnail when it asks
Immediately after a successful import, the app asks you for an image. Choose anything square — a frame from the clip, a poster, a screenshot. This is the picture that will appear on the effect's tile.
This step is why the thumb attribute has to exist. The app writes the path to the image
you pick into that attribute. No attribute, no picture.
The effect appears in the Custom list
Tap it and it applies to your selected layer, with all your sliders live in the parameter panel.
Leave the Custom page and come back in
Don't skip this. The grid is built when you enter the page. Backing out to the category list and re-entering Custom forces it to rebuild and register the new effect properly — count, thumbnails and all.
Import → back out → re-enter. If an effect looks like it didn't take, this is almost always why.
Repeat from step 4 for each effect. Deleting is the same in reverse — the app removes the file and its metadata from its own folder.
Controls — the sliders your users get
Everything inside <params> becomes a control in the parameter panel. Each one has an
id, and that id is the variable name you use in the shader. That's the
entire contract — same word in both places.
<spinner id="amount" default="0.5" min="0.0" max="1.0" step="0.01" label="Amount" />
^^^^^^
this becomes a variable called 'amount'
<shader type="fragment"><![CDATA[
void main(){
float v = amount; // use it directly - no declaration needed
}
]]></shader>
Using a name in the shader that isn't declared in <params> fails to compile, and the
effect quietly does nothing. Declare it, or don't use it.
The control types
spinner — a number slider
<spinner id="amount" default="0.7" min="0.0" max="2.0" step="0.01" label="Amount" />
<spinner id="angle" default="0" min="-180" max="180" step="0.5" label="Angle" type="angle" />
<spinner id="alpha" default="1.0" min="0.0" max="1.0" step="0.01" label="Alpha" type="percent" />
<spinner id="count" default="8" min="2" max="64" step="1" label="Count" type="integer" />type is cosmetic: percent shows 0–1 as a percentage,
angle adds a dial, integer snaps to whole numbers.
ticks="0, 0.5, 1" adds notch marks. slider is an older alias with
identical attributes.
color — a colour picker
<color id="tint" default="#FFFFC24B" label="Tint" alpha="true" />Format is #AARRGGBB — alpha first. In the shader it's a vec4, so use
tint.rgb for the colour.
switch — an on/off toggle
<switch id="vignetteOn" default="true" label="Add Vignette" />Arrives as a boolean. Use it directly: if (vignetteOn) { … }
selector — a dropdown or radio list
<selector id="mode" style="dropdown" default="0" label="Mode">
<choice value="0" label="Normal" />
<choice value="1" label="Multiply" />
<choice value="2" label="Screen" />
</selector>The id arrives as an integer holding the selected value.
texture — an image input
<texture id="inputImg" srcType="content" />
<texture id="comp" srcType="comp" />
<texture id="tmp" srcType="buffer" downsample="2" />
<texture id="grain" srcType="image" src="resource/grain.png" />| srcType | Meaning |
|---|---|
| content | The layer the effect sits on. Your inputImg. You almost always need exactly one. |
| comp | The whole composition underneath — for blend effects. |
| buffer | An internal render target for multi-pass work. downsample="2" halves the resolution. |
| image | A bundled image, referenced with src="…". |
point / xyz — vectors
<point id="center" default="0.5,0.5" label="Centre" />
<xyz id="rotate" default="0,0,0" label="Rotation" />A point is a vec2, an xyz is a
vec3.
section — a group header
<section text="Channel Gain" />Pure layout. It splits the panel into labelled groups. Fifteen ungrouped sliders is unusable — use these.
The shader — where the pixels happen
GLSL ES 1.00, the same language the app's own effects use. It runs once per pixel, on the GPU, every frame. You get the pixel position, you read the layer, you write a colour.
void main(){
vec2 uv = acScreenNorm; // where am I on screen, 0..1
vec4 src = texture2DCv(inputImg.texture, uv); // read the layer
gl_FragColor = src; // write it back
}
That is a complete, valid effect that does nothing. Start from it and add arithmetic in the middle.
Four rules that decide whether it works
| Rule | Why |
|---|---|
| Read with texture2DCv() | The engine's sampler clamps safely at the edges. texture2D() also works but doesn't clamp. |
| Output is premultiplied | Write vec4(rgb * alpha, alpha). Skip the multiply and semi-transparent pixels glow wrong. |
| Un-premultiply before grading | The input is premultiplied too. Do src.rgb / max(src.a, 1e-4) first, re-multiply at the end. |
| Helpers before main() | gmLum() is not built in — the app's own effects define it in the shader with float gmLum(vec3 c){ return dot(c, vec3(0.2126, 0.7152, 0.0722)); }. Calling it without defining it fails silently. |
An effect with everything in the right order
// Helper functions must be declared BEFORE main().
float luma(vec3 c){
return dot(c, vec3(0.2126, 0.7152, 0.0722)); // Rec.709
}
void main(){
vec2 uv = acScreenNorm;
vec4 src = texture2DCv(inputImg.texture, uv);
float a = max(src.a, 1e-4); // guard the divide
vec3 c = src.rgb / a; // un-premultiply -> straight colour
// 'amount' and 'speed' are ids declared in .
float pulse = 0.5 + 0.5 * sin(acTime * speed * 6.2831853);
c += amount * pulse * 0.4;
gl_FragColor = vec4(clamp(c, 0.0, 1.0) * a, a); // re-premultiply
}
Loop bounds must be constant. for (int i = 0; i < 8; i++) compiles everywhere;
for (int i = 0; i < count; i++) with a slider as the bound fails on many GPUs. Use a fixed
bound and multiply the result by the slider.
Values the app hands you
These exist automatically in every shader. You never declare them. They all start with ac,
which is how you tell engine values apart from your own parameters.
| Name | Type | What it is |
|---|---|---|
| acScreenNorm | vec2 | This pixel's position, 0..1. The one you'll use most. |
| acScreenSize | vec2 | Render target size in pixels. Use for aspect ratio. |
| acLayerNorm | vec2 | Position in the layer's own space. |
| acLayerSize | vec2 | Layer size in pixels. |
| acLayerToScreen | mat4 | Layer space to screen space. |
| acScreenToLayer | mat4 | The inverse. |
| acTime | float | Seconds. Feed it into a sin() to animate anything. |
| acPass | int | Which render pass is running. Only matters for multi-pass. |
| acLayerCenter | vec2 | Layer centre in pixels. |
| acLayerCenterNorm | vec2 | Layer centre, normalised. |
| acLayerPivot | vec2 | The layer's anchor point. |
| acPreviewSize | vec2 | Preview surface size. |
| acVelocity | vec2 | Layer motion — great for velocity-driven blur. |
| acScaleVelocity | float | Rate of scale change. |
| acAngularVelocity | float | Rate of rotation. |
| acShowGuides | bool | True while the user is dragging a control. |
float ar = acScreenSize.x / max(acScreenSize.y, 1.0); then work in
(uv - 0.5) * vec2(ar, 1.0). This is what keeps circles round on a tall phone.
Four things that save you an evening
Check the thumb attribute every single time
Before you import anything you wrote, look at the header and confirm thumb="…" is
there. It costs two seconds and it is the cause of more "my effect has no picture" messages than everything else
combined. Every file in the pack has it — use one as your starting point.
Keep 5–10 reference effects open while you work
Don't write GLSL into a blank page. Keep a handful of effects you like open in another window and copy their structure — how they declare controls, how they guard the alpha divide, how they lay out a loop. Strong effects are almost always a known-good skeleton with new maths in the middle. The 77-effect pack exists for exactly this: pick the one closest to your idea and change it.
If it isn't good, fix it again — don't ship it
Nobody gets a nice effect on the first pass. The loop is: edit → import → look at it on a real clip → change
one number → import again. Two dozen passes on one effect is normal. If the result is muddy, the fix is usually
a narrower slider range or a clamp() you forgot, not a rewrite.
Back out of Custom and re-enter after every import
The grid is built when you enter the page. Leave the Custom category and come back in, and the app rebuilds it — new effect counted, thumbnail loaded, everything in place. If an import seems to have done nothing, this is the first thing to try.
When it fails
Find the symptom you're seeing. Each one has a single cause.
“That XML is not a valid Gold Motion effect”
The app's parser rejected the file. In order of likelihood: a missing or mismatched closing tag, a raw
& or < inside an attribute value, a missing <?xml … ?> first
line, or a quote left open. Open the file in a browser — if it shows an XML error, that's your answer.
It imports, but the tile has no picture
The header is missing thumb="…". The app had nothing to write the image path into. Add the
attribute, re-import, and pick the thumbnail again.
“<name>” is already in your Custom effects.
You've imported an effect with the same name before. The app checks duplicates by name, not by
id. Either choose replace, or change the name in the header before importing.
The effect is in the list but does nothing
The shader didn't compile. Nine times out of ten a variable is used in the shader that isn't declared in
<params>, or the spelling differs between the two. Check every name matches exactly.
The sliders move but nothing changes
You declared the parameter but never used its id in the shader. The slider is wired to a variable nothing reads.
The labels are blank
You copied a stock effect and kept label="@am:string/…". Those strings live inside the app and
don't resolve in your file. Write literal text: label="Amount".
Edges look dark, or transparent areas look wrong
Premultiplied alpha. Un-premultiply with src.rgb / max(src.a, 1e-4) on the way in and output
vec4(rgb * a, a) on the way out.
It looks stretched on a tall phone
Aspect ratio. Work in (uv - 0.5) * vec2(acScreenSize.x / acScreenSize.y, 1.0) and convert back
before sampling.
It flickers, or the preview crawls
Too many texture reads. A 24-tap loop is 24 reads per pixel per frame. Cut the tap count, or declare a
buffer texture with downsample="2" and work at half resolution.
It worked yesterday and now it's gone
Custom effects live in the app's private storage. Clearing the app's cache or data removes them, as does
reinstalling. Keep your original .xml files somewhere safe — they are the master copies, and
re-importing takes seconds.
Generate a custom effect without guessing the format
This prompt is based on the schema used by all 77 XML effects in the pack: one content texture, grouped controls, a single fragment shader and the same premultiplied output convention. It gives the model the exact contract instead of asking it to invent Gold Motion attributes.
Copy the full block, add your effect description after EFFECT REQUEST:, and ask for
one XML file only. If the result fails to render, send the error back with “compare the shader with a working
effect from the pack and repair it”.
ROLE
You are a Gold Motion custom-effect developer. Produce a production-ready,
single-file XML effect that follows the exact conventions of the 77-effect
Gold Motion pack. Do not explain the file, add markdown, or output anything
besides the XML.
REQUIRED OUTPUT
Return exactly ONE complete file in this order:
<?xml version='1.0' encoding='UTF-8' ?>
<effect id="gm.custom.SLUG" name="NAME" desc="ONE SENTENCE"
category="custom"
thumb="content://com.goldmotion.pnkj_com.alightcreative.app.motion.fileprovider/share/gmcustom/SLUG.png"
tags="gm,custom,TAGS" experimental="false">
<params>
<texture id="inputImg" srcType="content" />
...grouped controls...
</params>
<shader type="fragment" precision="high"><![CDATA[
...complete GLSL ES 1.00...
]]></shader>
</effect>
HEADER RULES
1. Use lowercase id="gm.custom.SLUG". SLUG may contain letters, numbers and
underscores only; keep it short and unique.
2. Use category="custom" and experimental="false".
3. Include the thumb attribute exactly as shown. Gold Motion replaces its
value with the file URI for the image selected during import.
4. desc is one short sentence that explains the visual result, not the code.
5. tags is comma-separated and starts with gm,custom.
PARAMS RULES
1. Every file starts with <texture id="inputImg" srcType="content" />.
2. Use only these controls, because they match the pack:
<spinner id="x" default="0.5" min="0.0" max="1.0" step="0.01" label="Amount" />
Optional spinner types: type="percent", type="angle", type="integer".
<color id="tint" default="#FFFFC24B" label="Tint" alpha="true" />
<switch id="enabled" default="true" label="Enabled" />
<selector id="mode" style="dropdown" default="0" label="Mode">
<choice value="0" label="First mode" />
<choice value="1" label="Second mode" />
</selector>
<section text="Effect name" />
3. Give the effect 3 to 6 useful controls with realistic defaults and ranges.
Put its main controls first, then an Output group containing
<spinner id="mixAmt" default="1.0" min="0.0" max="1.0" step="0.01"
label="Effect Strength" />.
4. End params with
<section text="GOLD MOTION - CUSTOM FX" />.
5. Use literal labels. Never use "@am:string/..." or "@string/...".
6. Every shader variable must have a matching params id with exactly the
same spelling. Do not declare ac* engine values.
SHADER RULES
1. Use one fragment shader in GLSL ES 1.00. No #version, precision statement,
passes, multiple main functions, or external files.
2. Read the layer with:
vec2 uv = acScreenNorm;
vec4 src = texture2DCv(inputImg.texture, uv);
3. The sample is premultiplied. For normal colour processing use:
float a = max(src.a, 1e-4);
vec3 c = src.rgb / a;
...edit c...
gl_FragColor = vec4(clamp(c, 0.0, 1.0) * src.a, src.a);
4. Use texture2DCv(), never texture() or a guessed sampler function.
5. Every helper you call — including gmLum() — must be declared before
main(). Define gmLum as
float gmLum(vec3 c){ return dot(c, vec3(0.2126, 0.7152, 0.0722)); }
unless you use your own luma function. Loop bounds must be compile-time
constants. Keep loops short enough for phones; prefer 8-16 taps.
6. For circular coordinates use
vec2 p = (uv - 0.5) * vec2(acScreenSize.x / acScreenSize.y, 1.0);
7. Animated effects use acTime multiplied by a speed control. Static effects
must still be clearly visible at the default settings.
8. Multiply the final look by mixAmt. The default must show a useful,
obvious result rather than an empty or extremely subtle one.
QUALITY CHECK BEFORE ANSWERING
- The XML opens and closes correctly.
- No tags, attributes, ids or labels were invented outside this contract.
- Every control is used and every used variable is declared.
- The shader has no compile-time names that are undefined.
- The output has valid alpha and cannot produce transparent black edges.
- The file is suitable for a phone: modest texture sampling and no heavy
nested loops.
EFFECT REQUEST
Replace this line with the effect to create:
[Describe the visual result, intended use, animation, and controls here.]
Follow-ups that work
| Say this | To fix |
|---|---|
| “It renders black.” | “Compare the shader with a working pack effect. Check every helper such as gmLum() is defined before main(), the texture read uses texture2DCv, every params id matches, and the output re-multiplies the colour by alpha.” |
| “The tile is blank.” | “Restore the full thumb="content://…/gmcustom/SLUG.png" attribute in the effect header before import.” |
| “Edges look wrong.” | “The input is premultiplied. Un-premultiply with src.rgb / max(src.a, 1e-4) before colour maths and re-premultiply on output.” |
| “Make it move.” | “Drive it with acTime and add a speed control that multiplies it.” |
| “Too slow on my phone.” | “Cut the texture samples — replace the 24-tap loop with 8 taps and scale the result.” |
77 effects, ready to import
Nine categories, every header correct, every shader commented. Import them as they are, or open one and use it as the skeleton for your own. Nothing here is obfuscated.
The strongest Pro example from every category appears first.
Questions people actually ask
Do custom effects need internet or an account?
No. The file is parsed and stored on your device. There's no server, no key and no login.
Which Gold Motion version do I need?
V7.0.2 or newer. The Custom category and its + button don't exist in V6.x — that's the whole reason this feature is new.
Can one file hold several effects?
No. One file is one effect. Five effects means five files, imported one at a time.
Do I need to ship a thumbnail image with my effect?
Not in the file. You pick the thumbnail from your own gallery during import, and the app copies it into its own storage. The only requirement is that thumb="…" exists in the header so the app has somewhere to put the path.
Where do my imported effects actually live?
In the app's private custom_effects/ folder, each with a small .meta.json beside it. They're not in a shared folder you can browse to — which is why you should keep your original XML files safe.
Will my effects survive an app update?
Updating normally keeps app data, so yes. Clearing the app's data or uninstalling removes them. Re-importing from your saved XML files takes seconds.
Can a custom effect read the whole composition?
Yes — declare <texture id="comp" srcType="comp" />. Reading an arbitrary other layer isn't part of the custom schema.
Why does my effect look stretched on a tall phone?
Aspect ratio. Use acScreenSize.x / acScreenSize.y and work in normalised space, as shown in the shader section.
Can I sell effects I make with this?
The files you write are yours. The 77-effect pack here is free to use and learn from — if you pass it on, keep the files intact so nobody gets a broken import.