Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,31 @@ material.colorNode = t3.toTSL(() => {
</div>
</div>

Three.js textures can also be imported as typed texture handles. Pass a texture directly to
`fromTSL`, then use the regular TypeGPU texture functions to load or sample it:

```ts twoslash
import { d, std } from 'typegpu';
// ---cut---
import * as THREE from 'three/webgpu';
import * as TSL from 'three/tsl';
import * as t3 from '@typegpu/three';

const texture = new THREE.DataTexture(new Uint8Array(4), 1, 1);
const textureAccess = t3.fromTSL(texture, d.texture2d());
const samplerAccess = t3.fromTSL(TSL.sampler(texture), d.sampler());
const uvAccess = t3.uv();

const material = new THREE.MeshBasicNodeMaterial();
material.colorNode = t3.toTSL(() => {
'use gpu';
return std.textureSample(textureAccess.$, samplerAccess.$, uvAccess.$);
});
```

See the [interactive texture access example](/TypeGPU/examples#example=threejs--texture-access)
for a material that combines filtered `textureSample` colors with crisp `textureLoad` accents.

There are a handful of builtin TSL node accessors in the `t3` namespace:

<div class="flex flex-col md:flex-row gap-4 items-stretch md:items-center justify-between overflow-hidden">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<canvas></canvas>
151 changes: 151 additions & 0 deletions apps/typegpu-docs/src/examples/threejs/texture-access/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import * as THREE from 'three/webgpu';
import * as TSL from 'three/tsl';
import * as t3 from '@typegpu/three';
import { d, std } from 'typegpu';

const canvas = document.querySelector('canvas') as HTMLCanvasElement;

const renderer = new THREE.WebGPURenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.25;
await renderer.init();

const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
scene.fog = new THREE.FogExp2(0x030712, 0.055);

const camera = new THREE.PerspectiveCamera(42, canvas.clientWidth / canvas.clientHeight, 0.1, 100);
camera.position.set(0, 0.25, 10);

// A tiny color ramp created entirely on the CPU and owned by Three.js.
const colors = [
0x031b3d, 0x052b5c, 0x06467a, 0x075985, 0x087ea4, 0x0891b2, 0x06b6d4, 0x22d3ee, 0x5eead4,
0x99f6e4, 0xcffafe, 0x38bdf8,
];
const paletteData = new Uint8Array(colors.length * 4);
for (let i = 0; i < colors.length; i++) {
const color = colors[i];
paletteData.set([color >> 16, (color >> 8) & 0xff, color & 0xff, 0xff], i * 4);
}

const paletteTexture = new THREE.DataTexture(paletteData, colors.length, 1, THREE.RGBAFormat);
paletteTexture.colorSpace = THREE.NoColorSpace;
paletteTexture.magFilter = THREE.LinearFilter;
paletteTexture.minFilter = THREE.LinearFilter;
paletteTexture.wrapS = THREE.RepeatWrapping;
paletteTexture.needsUpdate = true;

// The Three.js texture and sampler become typed handles inside TypeGPU shaders.
const palette = t3.fromTSL(paletteTexture, d.texture2d());
const paletteSampler = t3.fromTSL(TSL.sampler(paletteTexture), d.sampler());
const position = t3.fromTSL(TSL.positionLocal, d.vec3f);
const normal = t3.fromTSL(TSL.normalLocal, d.vec3f);

const material = new THREE.MeshStandardNodeMaterial({
metalness: 0.72,
roughness: 0.23,
});

material.positionNode = t3.toTSL(() => {
'use gpu';
const preTransformedPosition = position.$;
const wave = std.sin(
std.dot(preTransformedPosition, d.vec3f(6.5, 4, 5.5) * 0.3) + t3.time.$ * 1.4,
);
return preTransformedPosition + normal.$ * wave * 0.055;
});

material.colorNode = t3.toTSL(() => {
'use gpu';
const localPosition = position.$;
const ripple =
std.sin(std.dot(localPosition, d.vec3f(2.8, -3.4, 4.2)) * 2.2 + t3.time.$ * 0.45) * 0.09;
const phaseA = std.fract(
std.dot(localPosition, d.vec3f(0.32, 0.18, 0.27)) + ripple + t3.time.$ * 0.025,
);
const phaseB = std.fract(
std.dot(localPosition, d.vec3f(-0.19, 0.36, 0.24)) - ripple * 0.7 - t3.time.$ * 0.018,
);

const flowingColorA = std.textureSample(palette.$, paletteSampler.$, d.vec2f(phaseA, 0.5));
const flowingColorB = std.textureSample(palette.$, paletteSampler.$, d.vec2f(phaseB, 0.5));
const weave = std.smoothstep(
-0.35,
0.65,
std.sin(std.dot(localPosition, d.vec3f(5.2, 7.5, -4.4)) + t3.time.$ * 0.18),
);
const wovenColor = std.mix(flowingColorA, flowingColorB, weave * 0.42);

const texelIndex = d.i32(std.floor(phaseA * colors.length));
const crispAccent = std.textureLoad(palette.$, d.vec2i(texelIndex, 0), 0);
const thread = std.smoothstep(
0.4,
0.5,
std.abs(std.fract(std.dot(localPosition, d.vec3f(1.8, 2.4, -2.1)) * 2.2 + ripple) - 0.5),
);

return std.mix(wovenColor, crispAccent * 1.3, thread * 0.3);
});

const knot = new THREE.Mesh(new THREE.TorusKnotGeometry(1.35, 0.42, 320, 64, 2, 3), material);
knot.rotation.x = -0.18;
scene.add(knot);

const halo = new THREE.Mesh(
new THREE.TorusGeometry(3.05, 0.02, 8, 160),
new THREE.MeshBasicMaterial({ color: 0xffaa22 }),
);
halo.rotation.x = Math.PI * 0.54;
halo.rotation.y = Math.PI * 0.08;
scene.add(halo);

const lightColor = 0xffffff;
scene.add(new THREE.AmbientLight(lightColor, 3.2));

const lightLeft = new THREE.PointLight(lightColor, 18, 12, 2);
lightLeft.position.set(-3.2, 2.4, 3.6);
scene.add(lightLeft);

const lightRight = new THREE.PointLight(lightColor, 18, 12, 2);
lightRight.position.set(3.2, -2.4, 3.6);
scene.add(lightRight);

let pointerX = 0;
let pointerY = 0;
const onPointerMove = (event: PointerEvent) => {
pointerX = event.clientX / window.innerWidth - 0.5;
pointerY = event.clientY / window.innerHeight - 0.5;
};
window.addEventListener('pointermove', onPointerMove);

const resizeObserver = new ResizeObserver(() => {
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
});
resizeObserver.observe(canvas);

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);
});
Comment on lines +130 to +136

function stdlibLerp(from: number, to: number, amount: number) {
return from + (to - from) * amount;
}

export function onCleanup() {
resizeObserver.disconnect();
window.removeEventListener('pointermove', onPointerMove);
paletteTexture.dispose();
knot.geometry.dispose();
material.dispose();
halo.geometry.dispose();
halo.material.dispose();
renderer.dispose();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"title": "Three.js - Texture access",
"category": "threejs",
"tags": ["3d", "ecosystem", "textures"],
"coolFactor": 5
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions packages/typegpu-gl/src/glslGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,22 @@ export class GlslGenerator extends WgslGenerator {
templateParams: readonly Snippet[],
args: readonly Snippet[],
): string {
if (name === 'textureLoad') {
return super.emitCall('texelFetch', templateParams, args);
}

if (name === 'textureSample') {
const [, sampler, coords, ...rest] = args;
if (!sampler || !coords) {
throw new Error(`Invalid number of arguments for 'textureSample'`);
}

// GLSL combines the texture and sampler into a single sampler uniform. The
// WebGL bridge therefore exposes the Three.js texture through the sampler
// argument and the separate WGSL texture argument is intentionally omitted.
return super.emitCall('texture', templateParams, [sampler, coords, ...rest]);
}

if (name === 'bitcast') {
const [target] = templateParams;
if (!target || !d.isWgslData(target.value)) {
Expand Down
31 changes: 31 additions & 0 deletions packages/typegpu-gl/tests/glslGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,37 @@ describe('GlslGenerator - variable declarations', () => {
});

describe('GlslGenerator - standard function calls', () => {
it('translates textureLoad() to texelFetch()', () => {
const texture = tgpu['~unstable'].rawCodeSnippet('palette', d.texture2d(), 'handle');

function loadTexel() {
'use gpu';
return std.textureLoad(texture.$, d.vec2i(2, 3), 0);
}

expect(tgpu.resolve([loadTexel], glOptions())).toMatchInlineSnapshot(`
"vec4 loadTexel() {
return texelFetch(palette, ivec2(2, 3), 0);
}"
`);
});

it('translates textureSample() to texture() with the combined sampler', () => {
const texture = tgpu['~unstable'].rawCodeSnippet('palette', d.texture2d(), 'handle');
const sampler = tgpu['~unstable'].rawCodeSnippet('paletteSampler', d.sampler(), 'handle');

function sampleTexture() {
'use gpu';
return std.textureSample(texture.$, sampler.$, d.vec2f(0.25, 0.75));
}

expect(tgpu.resolve([sampleTexture], glOptions())).toMatchInlineSnapshot(`
"vec4 sampleTexture() {
return texture(paletteSampler, vec2(0.25, 0.75));
}"
`);
});

it('translates scalar `select()` to ternary expression', () => {
function foo() {
'use gpu';
Expand Down
Loading
Loading