Skin and other scattering materials showed copies of the silhouette
running parallel to it: the blur's taps sit pixels apart when the mean
free path is wide on screen, and a tap that left the mesh dropped out
of the sum, so the light stepped at every tap spacing from the edge.
Taps are now shifted per pixel and per accumulated frame, and a tap
across a depth discontinuity takes the centre's light instead of
dropping out.
Fixes#86.
- Allocation checks never take an unanswered error scope as success:
native backends block on the verdict; the browser's WebGPU answers
later, so the full-size texture waits beside the bound placeholder
(tiles go into it under discarded scopes) and Renderer::prepare
installs it on success, or keeps the placeholder and reports the set
on failure. The UI keeps drawing frames while a verdict is pending.
- The GPU side wakes the UI when it records an uncaptured error or an
allocation failure, so the notice shows even when the app is idle;
failures are collected after prepare, where late verdicts land.
- Sets the GPU could not hold share one sticky notice (dismissible,
listing them) instead of a six-second toast each, since they are not
retried.
- The out-of-memory error is reported as the cause rather than the
follow-on invalid-texture error.
- The notice no longer uses "▸", which the UI fonts lack (egui panics
on a missing glyph).
Tests: a late texture stays unbound until accepted; a late rejection
keeps the placeholder and is reported once; a verdict that never comes
counts as a failure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Importers fan-triangulate quads and n-gons, and the 3D and UV wireframes drew
every triangle edge, so a quad mesh showed its triangulation. Meshes now carry a
per-triangle mask of the diagonals the importer added (Mesh::diagonals), and the
wireframe leaves them out (Mesh::wire_edges), unless a UV seam split one from its
twin.
- OBJ reads polygons whole (tobj without triangulation) and fans them itself.
- OBJ, PLY and the polygon sink (FBX, COLLADA, USD, Alembic) share one fan,
RawMesh::polygon, which records the diagonals.
- The built-in meshes mark their grid quads.
- glTF and STL store triangles only, so they keep every edge.
Fixes#41
Every texture set (one per mesh material) allocated full-size display
textures (base color, emissive, ORM, normal, height, aux, with mips) as
soon as it was created: ~130 MB of GPU memory per set at the default
2048 import size. A 16-material OBJ ran a 4 GB laptop GPU out of memory
and wgpu's default error handler panicked, closing the app.
- Display and aux textures start as 1×1 neutral placeholders and get
their full size with the first non-neutral tile (or aux image), so an
import costs no GPU memory until its sets are painted.
- That allocation runs in an error scope: on failure the texture keeps
its placeholder (no retry per tile), the failure is logged and the UI
shows a toast naming the texture set and the Viewport ▸ Texture
resolution setting. Painting and export are unaffected.
- The app installs an uncaptured-error handler that logs and reports
wgpu errors as toasts instead of panicking.
- The GPU path tracer takes each set's size from its largest texture,
since any kind (base color included) may now be a placeholder.
Tests: 40 sets of 2048 allocate nothing until painted, one painted kind
allocates alone, the aux texture allocates on its first upload, painted
placeholders render, and the GPU tracer matches the CPU with a default
base color beside varying roughness (fails with the old size logic).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The views' studio backdrop is a cool slate, lighter at the top, matching the panels around them (post pass, CPU and GPU path tracers), and the viewport token and its lettering follow it.
The shadow pass draws the dense copy and displaces it like the 3D view, so a displaced surface
shadows itself instead of being shadowed as if it were still smooth.
- shaders/displace.wgsl: displace_by_height, shared by mesh.wgsl and shadow.wgsl
- the shadow pipeline takes the set bind group (height and sampler) and a displacement vec4
(on, world units per unit of height with the height strength, vertex spacing)
- while it displaces, the cached shadow map is re-rendered when the set textures change
- test: with flat height, displaced shadows match the plain mesh's
Rendering mode traces displaced geometry when the display has displacement on, so path-traced
renders show the viewport's silhouettes and the displaced surface shadows itself.
- render::displace: the viewport's dense subdivision of the mesh, each vertex moved along its
normal by its set's height (box-filtered to the mip the vertex shader samples) times the
height strength, at the viewport's scale (world length per UV / 16)
- trace_geometry gives the GPU and CPU tracers the displaced mesh and BVH; Rendering mode,
texturecraft-cli render --samples and MCP viewport_render use it
- Rendering mode keeps the traced geometry until the mesh, a set's content or a height
strength changes (display tweaks don't rebuild it)
- subdivide::vertices shared by the renderer's mesh upload and displacement
- test: a uniform height lifts the dense plane by height x strength x world per UV / 16
- Code comments, crate descriptions, the MCP stroke schema and the Linux package descriptions
describe features in their own words instead of naming the reference application or other
vendors' products (file format names stay)
- The README follows the layout the other Crafting Apps use (VectorCraft): a look around, made
in TextureCraft, why TextureCraft, quick start with the agent setup, status, downloads per
platform, the Crafting Apps, star history, license; no trademark footer
- The detailed feature list moves to docs/features.md
- A new path-traced render of the built-in crate textured with smart materials
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The startup sample adds an empty paint layer on top of each stack and selects it (strokes on
the bottom layer were hidden under the smart material)
- Depth of field and subsurface blur read same-size targets by pixel, not by filtered UV, so
inert settings change nothing on any adapter (software adapters blended neighbours)
- CLI help lists .abc and builtin:udim_plane
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Strokes pre-blend their commit while painting and recompose committed tiles at low
priority: the 8K stroke-end frame drops from 30-42 ms to 12-19 ms
- Texture set resize runs in the background (8K: 3 s to 0.5 s, UV padding map from island
borders only)
- Decal move/scale/rotate handles; FBX and USD high-poly textures for the color map;
viewport displacement from the height channel; screen-space refraction of the scene
- CI pinned to Rust 1.96.0; FreeBSD VM sized for hosted runners; release builds embed
craft-fonts; file associations for FBX, COLLADA, USD and Alembic on every platform
- README, release checklist, NOTICE and attribution brought up to date
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Baking: per-map ray settings (AO, thickness, bent normals), GPU ray-traced baking with CPU
fallback (~12x), color map from textured glTF high polys
- Files: Alembic (Ogawa) and FBX 6 import, per-tile UDIM resolution, converting a set to UV
tiles, project templates, preserving painted strokes on mesh reimport
- UI: translations (French, German, Spanish, Portuguese, Italian, Japanese, Chinese, Korean)
with a live language picker and CJK fonts, closed panels reopen where they were, viewport
texture resolution setting, frame-budgeted scene sync for 8K sets
- Web: loading screen, WebGL2 notice, app-captured shortcuts, touch navigation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Rendering mode traces on the GPU (compute shaders, ~25x the CPU tracer) with CPU fallback;
CLI/MCP render --samples uses the GPU unless --cpu
- Depth of field in the viewport and the path tracer; multiple cameras with glTF import;
transmission/IOR, coat normal and screen-space subsurface blur; exact 8-bit normal decode
- Filter library (18 filters incl. warp, color balance, gradient map, posterize, MatFinish)
with a Filters shelf tab; geometry decals; clone from the current layer; anchors inside a
content stack; instances across texture-set resolutions; drag saved smart materials
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- UV tiles: auto-detected UDIM texture sets as one atlas per set, padding kept inside tiles,
one file per tile with the $udim export token, builtin:udim_plane
- USD: usda/usdc/usdz import (Crate reader with LZ4) and export with UsdPreviewSurface
- Shader lobes: clear coat, sheen, anisotropy and subsurface scattering channels in the
viewport and the path tracer; car paint, velvet, brushed steel, skin and wax materials;
glTF clearcoat/sheen export
- Color swatch popups no longer stay open
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An open-source, clean-room implementation of a Substance 3D Painter-style texturing
workflow, written in pure Rust with egui and wgpu.
- Layer stack with 32 blend modes, masks, generators, content effects and anchor points
- Painting, projection, smudge, clone, path, lasso fill, particle brush and quick mask
- 43 procedural materials, 17 smart materials and 10 smart masks
- Mesh map baking with high-poly, cage and match-by-name support
- PBR viewport with HDR environments, TAA and a path-traced Rendering mode
- glTF, FBX, COLLADA, OBJ, PLY and STL import; texture export presets; OBJ and GLB export
- Dockable Painter-style UI, shortcut editor, web build, MCP server and CLI
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>