Skip to content

WIP: Rainbow filament coloring by extrusion distance - #358

Draft
remcoder wants to merge 3 commits into
developfrom
feature/rainbow-filament
Draft

remcoder wants to merge 3 commits into
developfrom
feature/rainbow-filament

Conversation

@remcoder

Copy link
Copy Markdown
Member

Intent

Color filament by cumulative extrusion distance, computed per path and passed to the shader as a vertex attribute (extrusionDistance). Each geometry gets a per-vertex extrusion-distance attribute, and the material's fragment shader maps that value onto a rainbow hue.

Current state (WIP)

  • Path.geometry() now accepts an extrusionDistance parameter and returns { geometry, extrusionDistance }, setting a per-vertex extrusionDistance attribute.
  • createColorMaterial shader receives vExtrusion and maps it via mod(vExtrusion, 100.0) / 100.0 into a rainbow. The actual rainbow() mapping is stubbed — it returns red for t in (0,1) and white otherwise, so no real color gradient yet.
  • renderPathsAsTubes threads a running totalExtrusionDistance through each path and uses the returned geometry.
  • Uses a debug double-sided material (side: 2).

Fixes applied to make it compile

  • src/path.ts: gave geometry() a default value for the new extrusionDistance param and default opts so existing callers still type-check.
  • src/scene-manager.ts: guarded against the null result from path.geometry() before destructuring.
  • Prettier formatting on the WIP files so npm run lint passes.

npm run typeCheck and npm run lint both pass.

- give geometry() extrusionDistance a default so existing callers still type-check
- guard against null result before destructuring in renderPathsAsTubes
- prettier formatting for WIP files
@github-actions

github-actions Bot commented Aug 21, 2026 •

Copy link
Copy Markdown

Visit the preview URL for this PR (updated for commit e946a7e):

https://gcode-preview--pr358-feature-rainbow-fila-b1v3ppjg.web.app

(expires Sun, 20 Sep 2026 21:19:57 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: 59bd114ae4847b32c2bba0b68620b9069a3e3531

@sophiedeziel

Copy link
Copy Markdown
Collaborator

That looks like a cool feature to visualize gradient filament!

If the goal is to just have something pretty, gradient by layer may be simpler. But the case above is cool, but it can also help visualize what layers uses more filament and not ruin the effect (for example, a container with a flat bottom will have the colours to shift rapidly on the flat bottom and then have more of a stretched shift on the rest).

@remcoder

remcoder commented Aug 22, 2026 •

Copy link
Copy Markdown
Member Author

yeah this was an old experiment.

The idea was to have some sort of simulation bc it's always hard to predict how such prints come out. It would need a length after which the color change is 360 degrees and repeats.. assuming that this is a constant factor 🤷🏻 .

Your example of a container or base is a great example. But even just different sizes or number of instances of the same model change the coloring a lot.

And then there is the effect that 2 consecutive prints never look the same because they most probably never start at the same color. Let's say you print a rainbow Axolotl for a kid. Then a second kid wants a rainbow Axolotl too. However, the second Axolotl then becomes its own unique color variation, at which point the first kid now also wants a copy of the second version, and so on.... (ask me how I know 😄 )

@remcoder

Copy link
Copy Markdown
Member Author

actually doing this by layer might make it a lot easier! Thank for that idea!

@remcoder remcoder added the poc proof-of-concept. not intended te be merged into develop label Aug 22, 2026
@remcoder

Copy link
Copy Markdown
Member Author

Per-layer draw calls vs. per-vertex data + shader

Following the "doing this by layer might be a lot easier" thread — the by-layer idea is easier in one implementation and not easier in another, so it's worth separating the two. This came up while looking at topLayerColor and disableGradient for #356, which need the same machinery.

Numbers below are measured on demo/gcodes/3DBenchy.gcode — 240 layers, 3511 extrusion paths, 1 tool — with WebGLRenderer mocked, so they are CPU/geometry figures, not GPU.

Option A — a material and draw call per layer

Group geometry by layer, give each layer its own material, set its color directly.

For:

  • No shader work. Colors are ordinary uniforms, so the mapping lives in TypeScript where it is easy to iterate on — for a filament-simulation feature where the mapping is the thing you are tuning, that matters.
  • Works for lines as well as tubes. This is the big one. Tubes use our own createColorMaterial, but lines use three's LineMaterial, and renderTravel/line mode is the default path. Per-layer materials need nothing special from either.
  • Layer visibility becomes mesh.visible = false per layer instead of clipping planes: cheaper, exact at layer boundaries, and it would let us drop the -lineHeight / 2 offset hack in packLineVertices that exists only so clipping doesn't slice lines.
  • singleLayerMode and the layer range become trivially correct.

Against:

  • Draw calls scale with layer count. Today extrusion is 1 draw call per tool; per-layer makes it 240 on the Benchy, and real prints run 1000–3000 layers. At tens of microseconds of three.js CPU overhead per draw call, four-figure layer counts put you at risk of missing frame budget — every frame, not just on re-render.
  • Geometry fragments: line mode's single packed Float32Array becomes N buffers. For tubes it is worse, because BatchedMesh exists to batch and per-layer batches average ~15 paths on the Benchy.
  • Clipping updates become O(layers). updateClippingPlanesForShaderMaterials walks every material and updateLineClipping allocates a fresh Plane pair per LineSegments2, so a layer-slider drag goes from ~2 objects per tick to hundreds or thousands of allocations per tick — ironically making the slider worse, which per-layer visibility was supposed to make better.
  • cachedMaterials breaks this outright. createColorMaterial caches by color in a module-level map (src/helpers/colorMaterial.ts), so two layers with the same color get the same material object and their uniforms collide. A gradient happens to work because every layer's color differs; anything that repeats a color (a cyclic rainbow, notably — mod(distance, cycle) repeats by construction) silently shares materials. This cache also has to be dealt with for SceneManager never disposes its geometries: every render leaks a full model #375.
  • Interacts badly with the extrusionColor setter, which indexes materials by tool index.

Option B — per-vertex attribute, shader does the mapping

What this WIP already does: extrusionDistance as a per-vertex attribute, hue computed in the fragment shader.

For:

  • One draw call, regardless of layer count. Scales to any model.
  • Already proven in this repo, on this branch. attribute float extrusionDistance → varying float vExtrusion → rainbow(mod(vExtrusion, 100.0) / 100.0) works; only the rainbow() mapping is stubbed.
  • Continuous along the filament. Cumulative extrusion distance is genuinely continuous and does not respect layer boundaries — which is the actual physical thing being simulated. Per-layer coloring can only ever be a stepped approximation of it, and it cannot show the effect described upthread where a flat bottom burns through hue quickly and the walls stretch it out within a layer.
  • Retuning is a uniform update: cycle length, palette, offset all change without rebuilding geometry or re-rendering. That directly addresses the class of problem where topLayerColor needed a full render() to take effect.
  • The layer range could become an integer compare in the shader rather than a z-plane cut.

Against:

  • Lines need custom shader work. Tubes are fine — createColorMaterial is already a hand-written ShaderMaterial. Lines go through three's LineMaterial, so per-vertex color there means onBeforeCompile patching or forking the Line2 material, and instanced attributes rather than plain ones. This is the real cost of Option B and it is a maintenance cost, not a runtime one.
  • Attribute memory: tube geometries carry position + normal + uv = 8 floats per vertex, so one more float is about +12.5% on top of the 82 MB the Benchy's tube geometries already hold.
  • Per-vertex data is baked at geometry-build time. Changing the mapping is free; changing the source quantity (extrusion distance ↔ layer index) means rebuilding geometry.
  • Colors resolved in-shader are not readable back on the CPU, which matters if a legend or picking ever wants the value.

Option C — BatchedMesh.setColorAt, per path

Worth knowing it exists: three 0.185.1 (we allow >=0.166.0 <0.186.0) has BatchedMesh.setColorAt/getColorAt, and the renderer auto-enables it — batchingColor: IS_BATCHEDMESH && object._colorsTexture !== null sets USE_BATCHING_COLOR and multiplies vColor by getBatchingColor( getIndirectIndex( gl_DrawID ) ).

Per-path granularity, zero extra draw calls. Coarser than per-vertex (no gradient within a path) but finer than per-layer, and it would suit topLayerColor or a per-layer highlight well.

Caveat: that path runs through three's built-in shader chunks and terminates in vColor. createColorMaterial includes no chunks at all, so setColorAt would write the colors texture and our shader would never read it — it would need the includes added. (Relatedly, that material also ignores getBatchingMatrix and positions with plain modelViewMatrix; it only gets away with it because createBatchMesh never calls setMatrixAt, so every instance is identity.)

Suggestion for this issue

The by-layer idea does not require giving up the attribute approach — by-layer is a different input to the same shader, not a different architecture. Keep the per-vertex route and let a uniform select the mapping: cumulative distance for the honest filament simulation, layer index for the easy-to-reason-about version.

And for the by-layer variant specifically, no new attribute is needed at all: the shader already carries varying float vWorldY for clipping, so a by-layer hue can be derived from height directly. Caveat: that assumes uniform layer height, so it breaks under variable layer height and vase/spiral mode — a real layerIndex attribute is the robust form if that matters.

The one thing that would make Option A the right call is line mode. If per-vertex color for LineMaterial turns out to be more patching than it is worth, per-layer materials are the only approach that covers both renderers today — at which point the layer-count ceiling and the cachedMaterials collision both need addressing first.

Trade-offs only, no changes made here. Not affected by the WIP's stubbed rainbow() or its debug side: 2.

Assisted by Claude Code - Opus 5

This branch has not been deployed

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

Labels

poc proof-of-concept. not intended te be merged into develop

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants