Skip to content

Separate Open Brush-aware and static GLTF export profiles - #1117

Open
andybak wants to merge 101 commits into
mainfrom
feature/gltf-export-profiles
Open

Separate Open Brush-aware and static GLTF export profiles#1117
andybak wants to merge 101 commits into
mainfrom
feature/gltf-export-profiles

Conversation

@andybak

@andybak andybak commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

This consolidates the overlapping GLTF export work from #1081, #1109, and #1116 into one branch with two explicit export contracts:

  • newglb remains the Open Brush-aware format. It preserves the existing mesh and material semantics needed by clients that restore Open Brush shaders.
  • static-glb is the generic-consumer format. It bakes fixed-time geometry and shader appearance into standard GLTF data for clients such as Blender and other naive GLTF viewers.

Contract boundaries

Changes that are safe or backwards-compatible are shared by both profiles, including stable profile metadata, exporter contract metadata, correctness fixes, and material naming.

Open Brush-specific identity and shader restoration remain on the newglb path. Generic PBR approximation, emissive atlases, material deduplication, procedural texture baking, and fixed-time particle deformation are restricted to static-glb.

Static exports do not advertise TB_BrushGuid, and the material importer checks TB_ExportProfile, preventing an Open Brush-aware importer from applying live brush shaders to geometry that has already been baked.

The exporter-version-3 change proposed in #1081 is intentionally removed. Once breaking fixed-time deformation is isolated to static-glb, newglb does not require a breaking contract bump. Each profile now declares its own additive TB_ExportProfile and TB_ExporterContractVersion.

User impact

Users get a separate static-glb output for broad GLTF compatibility without weakening or changing the established newglb contract. The two outputs can be tested from a single branch and build.

Validation

  • Unity 2022.3.62f2 batch compilation completed successfully.
  • All added C# and compute shaders compiled without errors.
  • JSON manifests validated successfully.
  • Godot helper scripts passed Python syntax parsing.
  • Unity asset GUIDs and source-branch ancestry were verified.

Supersedes #1081, #1109, and #1116.

# Conflicts:
#	Assets/Scripts/UnityGLTF Plugins/OpenBrushExportPlugin.cs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a1bab61766

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +631 to +634
var clone = CloneGltfMaterial(source);
clone.EmissiveFactor = new GLTF.Math.Color(1f, 1f, 1f, 1f);
clone.EmissiveTexture = atlasTexInfo;
if (gain > 1f) ApplyEmissiveStrength(clone, gain, exporter);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the source texture in additive batch materials

When KeepStrokes is false, every textured additive brush replaces its source emissive texture with the 1×N stroke-color atlas. Because that atlas contains only a uniform color and is sampled through the newly injected UV channel, masks and patterns from _MainTex—for example on Fire, Stars, Embers, and similar brushes—no longer modulate emission, producing solid glowing geometry instead of the brush texture. The batch representation needs to combine the per-stroke color with the source texture rather than overwrite the latter.

Useful? React with 👍 / 👎.

Comment thread Assets/Scripts/Export/BrushBaker.cs Outdated
Comment on lines +158 to +162
if (TryGetStaticMapping(brushGuid, out var staticMapping))
{
Debug.Log(
$"[OB_STATIC_MESH] Applying static override {staticMapping.name} to brush {brushGuid}");
mesh = ProcessMesh(mesh, staticMapping);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass the exported object's transform to static compute bakes

For a brush under a translated, rotated, or scaled layer/object, the new static overrides are invoked without the supplied localToWorldMatrix; the shared overload instead binds the identity-root BrushBaker transform. The added Bubbles, Embers, Smoke, Snow, and related kernels explicitly calculate origins, noise, and displacement in world space, so their frozen snapshot is evaluated in layer-local coordinates and will differ from the scene's rendered state. Pass the mesh transform through the compute path and convert the resulting world-space positions back to mesh-local space before export.

Useful? React with 👍 / 👎.

Comment thread godot_export_v2.py Outdated
Comment on lines +286 to +289
# Build final .tres file
material_type = "StandardMaterial3D" if use_standard_material else "ShaderMaterial"

header = [f'[gd_resource type="{material_type}" format=3]']

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Attach shaders to generated ShaderMaterials

For every canonical material whose shader is outside the small standard-material allowlist, this writes a ShaderMaterial containing only parameters and texture resources, with no shader = ExtResource(...) assignment. In the committed dataset this path is taken by 74 of 132 Unity material variants, so those generated .tres files have no shader and cannot render their brushes when copied into Godot. Generate or reference a .gdshader resource before emitting these materials, or use a visible standard-material fallback.

Useful? React with 👍 / 👎.

andybak added 3 commits July 26, 2026 16:21
Tinted atlas tiles combine each stroke colour with the source brush texture so high emissive gains no longer wash patterned brushes into solid geometry.
Static compute bakes now evaluate with the exported object's matrices and convert world-space particle snapshots back to mesh-local positions, avoiding misplaced geometry under transformed layers.
The standalone generators and their derived brush dataset are unrelated to the export-contract split and emit unusable ShaderMaterials without shader resources, so they should not expand this PR's scope.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c4a1267f80

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +624 to +625
sourceUv.x = ApplyTextureWrap(sourceUv.x, textureSource.Texture);
sourceUv.y = ApplyTextureWrap(sourceUv.y, textureSource.Texture);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve UV continuity across atlas wrap seams

For repeating additive textures, wrapping each vertex independently breaks interpolation whenever a triangle crosses an integer UV boundary: endpoints such as 0.9 and 1.1 become 0.9 and 0.1, so the rasterizer traverses most of the atlas tile instead of crossing the texture seam. Long brush ribbons commonly use continuously increasing UVs, causing their patterns to stretch or run backward at every repeat; split geometry at wrap boundaries or otherwise retain continuous coordinates when constructing the atlas.

Useful? React with 👍 / 👎.

Comment on lines +68 to +75
float seed = color.a;
float t01 = fmod(kEmbersExportTime * kScrollRate + seed * 10.0, 1.0);
float floatUpTime01 = t01;
float3 disp = kScrollDistance * t01;
disp.x += sin(t01 * kScrollJitterFrequency + seed * 100.0 + kEmbersExportTime + originWS.z) * kScrollJitterIntensity;
disp.y += (fmod(seed * 100.0, 1.0) - 0.5) * kScrollDistance.y * floatUpTime01;
disp.z += cos(t01 * kScrollJitterFrequency + seed * 100.0 + kEmbersExportTime + originWS.x) * kScrollJitterIntensity;
originWS.xyz += kSpreadProgress * disp * kDecimetersToWorldUnits;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bake Embers' per-particle color animation

At the fixed export time, t01 and seed also drive the large sparkle, lifetime fade, and brightness changes in Assets/Resources/Brushes/Basic/Embers/Embers.shader lines 139-152, but this kernel only moves vertices and the prefab sets ModifyColor to false. Consequently the static material atlas applies only the stroke color, leaving particles fully emissive when the live snapshot would make them dim or invisible; write the fixed-time RGB/alpha result and arrange for the static material to consume it.

Useful? React with 👍 / 👎.

Comment on lines +1397 to +1403
if (!material.SetPass(bakePass))
{
Debug.LogWarning(
$"[OB_GLTF_BAKE] Shader pass {bakePass} is unavailable for material {material.name}");
return null;
}
Graphics.DrawMeshNow(bakeMesh, Matrix4x4.identity);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Freeze animated shader inputs during texture baking

When baking the Neon Pulse and Electricity policies, this renders the live material without enabling its _TimeBlend override or neutralizing audio globals. Those shaders read GetTime() and _BeatOutput, so identical sketches exported at different instants can produce different baked textures, and the texture phase can disagree with geometry explicitly frozen at time zero (or be unusable during a quiet audio frame). Render a temporary material with a fixed time override and deterministic neutral audio inputs before calling SetPass.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 61494991d7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +694 to +695
float atlasU = (tileX + sourceUv.x) / columns;
float atlasV = (tileY + sourceUv.y) / rows;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add filtering gutters between emissive atlas tiles

When a batch contains multiple stroke colors and the source texture uses bilinear or trilinear filtering, mapping each tile over its full [0,1] range places UV endpoints directly on the boundary between differently tinted tiles. Common brush vertices with UV 0 or 1 will therefore sample neighboring stroke colors, producing colored seams; inset the coordinates by half a texel and duplicate edge pixels into gutters.

Useful? React with 👍 / 👎.

Comment on lines +786 to +790
atlasPixels[atlasRow + x] = new Color32(
(byte)(pixel.r * tint.r / 255),
(byte)(pixel.g * tint.g / 255),
(byte)(pixel.b * tint.b / 255),
pixel.a);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fold bubble highlight alpha into the atlas RGB

For batched Bubbles and Rising Bubbles, their shaders add _MainTex.a to RGB as an untinted white highlight, but this atlas stores only pixel.rgb * tint in RGB and leaves the highlight in alpha. glTF emissive textures consume RGB rather than alpha, so every KeepStrokes=false static export loses the bubbles' characteristic white highlight; bake that alpha contribution into the atlas RGB for these brushes.

Useful? React with 👍 / 👎.


// Bake a fixed-time snapshot of Snow's animated center displacement.
// The runtime shader animates this with GetTime(); export uses time 0 as a static mesh substitute.
float t = fmod(kSnowExportTime * kScrollRate + color.a, 1.0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bake Snow's fixed-time lifetime alpha

For Snow particles, this t also drives o.color.a = pow(1 - abs(2 * (t - .5)), 3) in Snow.shader, which controls the brush's SrcAlpha One lifetime fade. The static mapping leaves ModifyColor false and this kernel writes only positions, so the exported mesh retains the random seed in vertex alpha instead of the opacity at the frozen phase, making particles incorrectly bright or dim; write the computed alpha and enable color readback.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4b254ea086

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1341 to +1342
pbr.BaseColorTexture = bakedInfo;
pbr.BaseColorFactor = ToGltfColor(Color.white);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route unlit additive bakes through the emissive texture

For static exports of Electricity and Neon Pulse, whose policies use UvUnlit, this stores the baked center-line/pulse only in baseColorTexture. AfterPrimitiveExport subsequently handles these additive materials by cloning them with an emissive texture sourced from raw _MainTex/the color atlas (or no texture for Neon Pulse), so the emissive factor illuminates the whole ribbon and the baked procedural pattern never modulates emission. Export the UvUnlit result as the emissive texture, including when constructing per-stroke color atlases.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a3ba6a30b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +631 to +632
clone.EmissiveFactor = new GLTF.Math.Color(r, g, b, 1f);
if (gain > 1f) ApplyEmissiveStrength(clone, gain, exporter);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply sub-unit emissive gain to additive materials

For additive brushes whose configured emission gain is below 1, this condition drops the gain entirely: Comet uses m_EmissiveFactor: 0.5, Sparks uses 0.2, and Rain uses 0.3, but their cloned materials receive the unscaled stroke color as EmissiveFactor. The atlas path has the same behavior with a white factor, so both KeepStrokes modes render these brushes two to five times brighter than configured; multiply the emissive factor by gains up to 1 and reserve KHR_materials_emissive_strength for values above 1.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3912498dd1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1058 to 1060
exporter.DeclareExtensionUsage(EXT_blend_operations.EXTENSION_NAME, false);
AddExtension(materialNode, EXT_blend_operations.Add);
materialNode.AlphaMode = AlphaMode.BLEND;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Provide a portable fallback for additive blending

For static-glb files opened by generic glTF consumers such as Blender, this optional repository-specific EXT_blend_operations extension is ignored, leaving only AlphaMode.BLEND. Standard alpha blending attenuates/replaces the destination color, whereas these brushes require One One or SrcAlpha One, so Fire, Stars, Embers, Smoke, Snow, and other additive strokes render incorrectly over non-black backgrounds. Because this profile is explicitly intended for generic consumers, its baked material data needs a core-compatible approximation that does not depend on this custom extension.

Useful? React with 👍 / 👎.

# Conflicts:
#	Assets/Scripts/UnityGLTF Plugins/OpenBrushExportPlugin.cs
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T13:57:57.742640Z 294cac4 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 294cac470f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

{
// Static export can deduplicate brush materials because all
// per-stroke colour is represented in exported mesh/material data.
batch.gameObject.GetComponent<Renderer>().sharedMaterial = brush.Material;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve runtime material overrides in static exports

When a sketch uses Stroke.SetShaderFloat, SetShaderColor, SetShaderTexture, clipping, or the equivalent Layer API, those values and keywords are stored on Batch.InstantiatedMaterial. Replacing it with the catalog's brush.Material before UnityGLTF processes the batch discards those overrides, so the static GLB exports default textures and parameters rather than the currently rendered appearance; restoring the instance afterward only repairs the live scene. Export the instantiated material, or deduplicate only instances with identical effective properties.

Useful? React with 👍 / 👎.

float uv0Sum = uv0.x + uv0.y;
float uv0Dif = uv0.x - uv0.y;

float3 preBillboard = float3(-length, length, 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply stored particle rotation when baking billboards

For Snow particles with a nonzero uv0.z rotation, this reconstructs every quad as an unrotated axis-aligned square. The live shader passes that component as rotation to OrientParticle, so asymmetric snowflake textures are visibly reoriented in the static export. The same reconstruction is repeated in the Bubbles, Embers, and Rising Bubbles kernels; rotate the baked corner offset by the stored angle before writing the vertex.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant