Skip to content

feat(@typegpu/three): fromTSL() with textures and samplers - #2852

Open
iwoplaza wants to merge 2 commits into
feat/std-intdivfrom
feat/typegpu-three-load-texture
Open

feat(@typegpu/three): fromTSL() with textures and samplers#2852
iwoplaza wants to merge 2 commits into
feat/std-intdivfrom
feat/typegpu-three-load-texture

Conversation

@iwoplaza

Copy link
Copy Markdown
Collaborator

No description provided.

Copilot AI lite review requested due to automatic review settings August 15, 2026 18:32
@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown

pkg.pr.new

packages
Ready to be installed by your favorite package manager ⬇️

https://pkg.pr.new/software-mansion/TypeGPU/typegpu@50d3d3e610ef18c749210e69cf6a6c7b98e05b16
https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/gl@50d3d3e610ef18c749210e69cf6a6c7b98e05b16
https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/three@50d3d3e610ef18c749210e69cf6a6c7b98e05b16

benchmark
view benchmark

commit
view commit

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds support and documentation for importing Three.js textures into TypeGPU shaders as typed “handle” resources, enabling textureLoad / textureSample usage via fromTSL.

Changes:

  • Extend fromTSL / TSLAccessor to treat WGSL resource types (textures/samplers) as handle outputs when building TSL nodes.
  • Add Vitest coverage for texture load/sample using Three.js DataTexture + TSL sampler.
  • Add a new docs example and update ecosystem docs to demonstrate texture handle access.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/typegpu-three/src/typegpu-node.ts Adds resource-output detection and routes accessor builds through handle outputs for textures/samplers.
packages/typegpu-three/tests/typegpu-node.test.ts Adds mocks/helpers and tests ensuring generated fragment code/uniforms include texture access.
apps/typegpu-docs/src/examples/threejs/texture-access/meta.json Registers a new interactive docs example.
apps/typegpu-docs/src/examples/threejs/texture-access/index.ts Implements the interactive Three.js WebGPU texture access demo using fromTSL handles.
apps/typegpu-docs/src/examples/threejs/texture-access/index.html Adds the example canvas container.
apps/typegpu-docs/src/content/docs/ecosystem/typegpu-three/index.mdx Documents importing Three.js textures via fromTSL and using TypeGPU texture ops.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

const builder = builderFor('generate');
expect(() => node.build(builder)).not.toThrow();
expect(builder.getCodes('fragment')).toContain('textureSample');
expect(fragmentUniforms(builder)).toHaveLength(1);
Comment on lines +129 to +155
function getResourceOutput(dataType: d.AnyWgslData): string | undefined {
if (dataType.type === 'sampler') {
return 'sampler';
}
if (dataType.type === 'sampler_comparison') {
return 'samplerComparison';
}
if (dataType.type === 'texture_external') {
return 'texture';
}
if (dataType.type.startsWith('texture_storage')) {
return 'storageTexture';
}
if (dataType.type.startsWith('texture_depth')) {
return 'depthTexture';
}
if (dataType.type === 'texture_cube' || dataType.type === 'texture_cube_array') {
return 'cubeTexture';
}
if (dataType.type === 'texture_3d') {
return 'texture3D';
}
if (dataType.type.startsWith('texture_')) {
return 'texture';
}
return undefined;
}
import * as TSL from 'three/tsl';
import * as t3 from '@typegpu/three';

const texture = new THREE.DataTexture(new Uint8Array(4), 1, 1);
pointerX = event.clientX / window.innerWidth - 0.5;
pointerY = event.clientY / window.innerHeight - 0.5;
};
window.addEventListener('pointermove', onPointerMove);
Comment on lines +130 to +136
void renderer.setAnimationLoop((time) => {
const seconds = time * 0.001;
knot.rotation.y = seconds * 0.16 + pointerX * 0.3;
knot.rotation.x = -0.18 + stdlibLerp(knot.rotation.x + 0.18, -pointerY * 0.2, 0.04);
halo.rotation.z = -seconds * 0.055;
renderer.render(scene, camera);
});
knot.geometry.dispose();
material.dispose();
halo.geometry.dispose();
halo.material.dispose();
@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown

Resolution Time Benchmark

---
config:
  themeVariables:
    xyChart:
      plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
  title "Random Branching (🔴 PR | 🔵 main | 🟢 release)"
  x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
  y-axis "time (ms)"
  line [1.05, 2.06, 4.39, 6.95, 7.65, 12.14, 23.91, 25.04]
  line [0.98, 2.05, 4.38, 6.94, 8.14, 13.07, 26.46, 24.81]
  line [1.03, 2.03, 4.43, 6.50, 7.66, 12.59, 25.99, 24.81]
Loading
---
config:
  themeVariables:
    xyChart:
      plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
  title "Linear Recursion (🔴 PR | 🔵 main | 🟢 release)"
  x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
  y-axis "time (ms)"
  line [0.31, 0.57, 0.73, 0.84, 1.15, 1.19, 1.47, 1.64]
  line [0.29, 0.49, 0.68, 0.81, 1.10, 1.14, 1.37, 1.56]
  line [0.29, 0.52, 0.71, 0.88, 1.48, 1.15, 1.43, 1.60]
Loading
---
config:
  themeVariables:
    xyChart:
      plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
  title "Full Tree (🔴 PR | 🔵 main | 🟢 release)"
  x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
  y-axis "time (ms)"
  line [0.91, 2.44, 4.51, 7.64, 13.48, 26.82, 57.18, 115.69]
  line [0.89, 2.24, 4.36, 7.56, 12.73, 26.54, 57.56, 114.35]
  line [0.86, 2.16, 3.86, 7.50, 12.65, 26.57, 56.24, 114.01]
Loading

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown

Bundle size comparison (import * as ... in PR vs import * as ... in target):

🟢 Decreased ➖ Unchanged 🔴 Increased (max 0.18%) ❔ Unknown
0 311 13 0

import { ... } in PR vs import * as ... in PR (is the library tree-Shakeable?):

Test tsdown
tgpu_init.ts 274.51 kB ($${\color{green}-3.4\%}$$)
tgpu_initFromDevice.ts 273.97 kB ($${\color{green}-3.6\%}$$)
tgpu_resolve.ts 174.80 kB ($${\color{green}-38.5\%}$$)
tgpu_resolveWithContext.ts 174.74 kB ($${\color{green}-38.5\%}$$)
tgpu_bindGroupLayout.ts 74.47 kB ($${\color{green}-73.8\%}$$)
tgpu_mutableAccessor.ts 69.19 kB ($${\color{green}-75.6\%}$$)
tgpu_accessor.ts 69.19 kB ($${\color{green}-75.6\%}$$)
tgpu_privateVar.ts 67.88 kB ($${\color{green}-76.1\%}$$)
tgpu_workgroupVar.ts 67.88 kB ($${\color{green}-76.1\%}$$)
tgpu_const.ts 67.30 kB ($${\color{green}-76.3\%}$$)
tgpu_lazy.ts 67.10 kB ($${\color{green}-76.4\%}$$)
tgpu_fragmentFn.ts 39.65 kB ($${\color{green}-86.0\%}$$)
tgpu_fn.ts 39.60 kB ($${\color{green}-86.1\%}$$)
tgpu_vertexFn.ts 39.47 kB ($${\color{green}-86.1\%}$$)
tgpu_computeFn.ts 39.17 kB ($${\color{green}-86.2\%}$$)
tgpu_vertexLayout.ts 28.30 kB ($${\color{green}-90.0\%}$$)
tgpu_comptime.ts 15.91 kB ($${\color{green}-94.4\%}$$)
tgpu_unroll.ts 1.75 kB ($${\color{green}-99.4\%}$$)
tgpu_slot.ts 1.70 kB ($${\color{green}-99.4\%}$$)

If you wish to run a comparison for other, slower bundlers, run the 'Tree-shake test' from the GitHub Actions menu.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ Minor suggestions only — the resource-handle path is sound, the mapping checks out against three.js internals, and the new tests are meaningful.

Reviewed changes

  • fromTSL(THREE.Texture, d.WgslTexture) now accepts a raw THREE.Texture (auto-wrapped via TSL.texture) and the node path builds resources directly, alongside a getResourceOutput() helper that maps WGSL data types to three.js reference outputs.
  • TSLAccessor treats resource types (sampler, texture*, depthTexture, cubeTexture, storageTexture, texture3D) through node.build(builder, resourceOutput) + a handle-origin rawCodeSnippet, bypassing the scalar bridge variable.
  • A new docs example + index.mdx section demonstrating textureSample/textureLoad against a Three.js DataTexture.

I verified the resource outputs against three@0.181.2: every value getResourceOutput() emits is a member of WGSLNodeBuilder.isReference, so building a TextureNode with that output returns the materialized handle instead of emitting a sample snippet. A single texture uniform in three's WGSL builder emits both the texture and its _sampler bindings, which explains why the sampler test's toHaveLength(1) is correct rather than accidental. The bridge-var path is unchanged for non-resource types (#resourceOutput is undefined), so existing storage-buffer/scalar accessors are unaffected. Tests and tsc both pass locally.

ℹ️ Nitpicks

  • The storage path in generate still looks up getBridgeVar(dep, builder) for every dependency (including resource handles, whose #var is now undefined), so the loop runs a no-op continue for them. Harmless, just a small structural wart — a comment or a if (!this.#resourceOutput) guard before the bridgeVar lookup would make intent clearer.
  • The new texture-access example defines its own stdlibLerp; THREE.MathUtils.lerp already exists if you want to drop the helper.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏


export const fromTSL = tgpu.comptime(((node, type) => {
const tgpuType = d.isData(type) ? type : (type as (length: number) => d.AnyWgslData)(0);
) => TSLAccessor<T, TNode>) &

@pullfrog pullfrog Bot Aug 15, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: the convenience overload is typed T extends d.WgslTexture, which excludes WgslStorageTexture and WgslExternalTexture (separate interfaces in typegpu's texture schemas), even though getResourceOutput() explicitly maps texture_storage_* to storageTexture and texture_external to texture. So fromTSL(someTexture, d.textureStorage2d('rgba8unorm')) or fromTSL(texture, d.externalTexture()) won't type-check, and casting past the type wouldn't help either, because the runtime auto-wraps with TSL.texture, a sampling TextureNode, which is not a usable storage/external binding. Consider widening the overload to also accept WgslStorageTexture / WgslExternalTexture and routing those through TSL.storageTexture(...) / an external-texture node, or documenting that only sampled and depth textures are supported via the THREE.Texture overload and everything else must be passed as a TSL node.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important

The new WebGL fallback mapping for textureSample / textureLoad only handles the plain scalar-coordinate case and silently emits invalid GLSL for offset, array, cube, and depth forms — valid std calls fail to compile at runtime instead of producing a clear error. The GL fallback also lands as a TEMP: commit, so it reads as work-in-progress; worth deciding on coverage (guard unsupported forms or map them correctly) before this is merged.

Reviewed changes

  • Routed std.textureSample / std.textureLoad codegen through ctx.gen.emitCall(...), opening the GLSL generator override hook while keeping the WGSL output byte-identical to the previous stitch template (verified against generated WGSL).
  • Taught GlslGenerator.emitCall to translate textureLoadtexelFetch and textureSampletexture, dropping the separate WGSL texture argument where GLSL fuses texture and sampler into a single uniform.
  • Extracted TSLAccessor.buildAccessorNode, and in WebGL mode strips the trailing _sampler from sampler-accessor snippets so they reference the fused GLSL sampler uniform.
  • Added WebGL-backend coverage in @typegpu/three and @typegpu/gl for the combined-sampler load/sample cases.

I verified against three@0.181.2 that the WGSL and GLSL builders both emit nodeUniform0_sampler for the sampler snippet (so the _sampler strip is load-bearing), that WebGLBackend sets isWebGLBackend = true and pairs with GLSLNodeBuilder, and that the generated texture(...) / texelFetch(...) pass through three's GLSL node parser as literal GLSL. All tests (typegpu-three: 7, typegpu-gl glslGenerator: 25, typegpu texture: 40) pass.

⚠️ GLSL fallback only supports the plain 2D/3D/cube sampled and loaded forms

Beyond the tested happy path, the new textureSample / textureLoad translation emits uncompilable GLSL with no guard. Confirmed by direct generation: textureSample with the documented offset?: v2i argument becomes texture(nodeUniform0, vec2(0.5), ivec2(1, 0)) (GLSL texture()'s third parameter is a float bias), and an array textureLoad becomes texelFetch(nodeUniform0, uvec2(0), 0, 0) (GLSL texelFetch needs a fused ivec3 coordinate and LOD). Cube and depth sampling are broken the same way (cubemap load needs a fused ivec3; depth textures are sampler2DShadow, which texture(sampler, vec2) can't address). See the inline comment on GlslGenerator.emitCall for the details and suggested mappings.

ℹ️ Nitpicks

  • The trailing _sampler strip (snippet.replace(/_sampler$/, '')) is a silent no-op if three ever stops suffixing sampler snippets with _sampler (or if the built snippet is a longer expression) — it degrades to emitting a wrong GLSL name rather than failing loudly. A comment asserting the invariant is already there, which helps, but a !endsWith('_sampler') guard that throws would make failure explicit.
  • args.filter((arg) => arg !== undefined) in the new textureSample codegen is dead code — dualImpl passes fully converted snippets, never undefined. Harmless, just noise.
  • Commit title TEMP: GL fallback — if the GL path is meant to ship, it deserves a real title and a coverage decision; if it's exploratory, it shouldn't be on the PR branch.

Pullfrog  | Fix it ➔View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@iwoplaza
iwoplaza force-pushed the feat/typegpu-three-load-texture branch from 9464302 to 50d3d3e Compare August 17, 2026 18:35
@iwoplaza
iwoplaza changed the base branch from main to feat/std-intdiv August 17, 2026 18:36
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.

2 participants