Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TSL: add support for interpolation type qualifiers such as centroid #30576

Open
isaac-mason opened this issue Feb 21, 2025 · 6 comments
Open

Comments

@isaac-mason
Copy link
Contributor

isaac-mason commented Feb 21, 2025

Description

It appears it's not currently possible to create a varying with TSL with a interpolation type qualifier, for example in GLSL, centroid.

centroid varying vec2 uvCentroid

In three.js WebGLRenderer projects I have used centroid to work around issues with texture bleeding when using texture atlases with MSAA.

Solution

Consider exposing common interpolation qualifiers via TSL.

I don't have good concrete API suggestions, but here's a simple example of how I'd use this:

const uvCentroid = uv().interpolation("centroid")
const color = texture(map, uvCentroid)

Alternatives

Some other general API for exposing common qualifiers.

Additional context

GLSL: Center or Centroid? (Or When Shaders Attack!) - https://opengl.org/pipeline/article/vol003_6
https://www.w3.org/TR/WGSL/#interpolation-sampling-centroid

@cmhhelgeson
Copy link
Contributor

It appears it's not currently possible to create a varying with TSL with a interpolation type qualifier, for example in GLSL, centroid.

centroid varying vec2 uvCentroid

In three.js WebGLRenderer projects I have used centroid to work around issues with texture bleeding when using texture atlases with MSAA.

I'll try implementing this. Do you have a specific project or test case we could test the implementation against?

@isaac-mason
Copy link
Contributor Author

isaac-mason commented Feb 21, 2025

Amazing @cmhhelgeson! I'm happy to help however I can.

I can share a voxel demo that uses WebGPURenderer and exhibits an issue that should be mitigatable with centroid (I have mitigated this issue with centroid on very similar WebGLRenderer projects):

https://codesandbox.io/p/devbox/8vxpqg

I can put this on github or create a more minimal javascript example if that's helpful - let me know.


  • You can find the material that samples from the texture atlas in src/lib/chunk-material.ts
  • Note the artifacts at the edges of faces when MSAA is enabled (WebGPURenderer is created in src/main.ts)
    • antialiasing: true
      • Image
    • antialiasing: false
      • Image

@isaac-mason
Copy link
Contributor Author

isaac-mason commented Feb 21, 2025

Here's a more minimal demo that we can use:

Image

<html lang="en">

<head>
    <title>three.js webgpu - centroid</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
</head>
<style>
    body {
        margin: 0;
        overflow: hidden;
        width: 100vw;
        height: 100vh;
    }

    #demo {
        display: flex;
        flex-direction: row;
        align-items: center;
    }

    .renderer-wrapper {
        display: flex;
        flex-direction: column;
        align-items: center;
    }

    #antialising-disabled {
        border-right: 1px solid black;
    }

    canvas {
        width: 100%;
        height: 100%;
    }
</style>

<body>
    <div id="demo">
        <div id="antialising-disabled" class="renderer-wrapper">
            <div>antialising disabled</div>
        </div>
        <div id="antialising-enabled" class="renderer-wrapper">
            <div>antialising enabled</div>
        </div>
    </div>
    <script type="importmap">
      {
        "imports": {
          "three": "https://unpkg.com/[email protected]/build/three.module.js",
          "three/webgpu": "https://unpkg.com/[email protected]/build/three.webgpu.js",
          "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/",
          "three/tsl": "https://unpkg.com/[email protected]/build/three.tsl.js"
        }
      }
    </script>
    <script type="module">
        import * as THREE from "three/webgpu";
        import { uv, color, Fn, mix, sqrt, texture } from "three/tsl";
        import { OrbitControls } from "three/addons/controls/OrbitControls.js";

        let rendererAntialisingEnabled;
        let rendererAntialisingDisabled;
        let renderer;
        let camera;
        let scene;
        let orbitControls;

        const atlasCanvas = document.createElement("canvas");

        // make simple example texture atlas with 4 tiles
        atlasCanvas.width = 16;
        atlasCanvas.height = 16;

        const ctx = atlasCanvas.getContext("2d");

        ctx.fillStyle = "red";
        ctx.fillRect(0, 0, 8, 8);

        const redUVs = [0, 1, 0.5, 1, 0.5, 0.5, 0, 0.5];

        ctx.fillStyle = "green";
        ctx.fillRect(8, 0, 8, 8);

        const greenUVs = [1, 1, 0.5, 1, 0.5, 0.5, 1, 0.5];

        ctx.fillStyle = "blue";
        ctx.fillRect(0, 8, 8, 8);

        const blueUVs = [0, 0, 0.5, 0, 0.5, 0.5, 0, 0.5];

        ctx.fillStyle = "yellow";
        ctx.fillRect(8, 8, 8, 8);

        const yellowUVs = [1, 0, 0.5, 0, 0.5, 0.5, 1, 0.5];

        const faces = [redUVs, greenUVs, blueUVs, yellowUVs];

        document.body.appendChild(atlasCanvas);
        atlasCanvas.style.position = "absolute";
        atlasCanvas.style.bottom = "10px";
        atlasCanvas.style.left = "10px";
        atlasCanvas.style.width = "200px";
        atlasCanvas.style.height = "200px";
        atlasCanvas.style.zIndex = "1000";
        atlasCanvas.style.imageRendering = "pixelated";
        atlasCanvas.style.border = "1px solid black";

        const canvasTexture = new THREE.CanvasTexture(atlasCanvas);
        canvasTexture.colorSpace = THREE.SRGBColorSpace;
        canvasTexture.mapping = THREE.UVMapping;
        canvasTexture.wrapS = THREE.RepeatWrapping;
        canvasTexture.wrapT = THREE.RepeatWrapping;
        canvasTexture.magFilter = THREE.NearestFilter;
        canvasTexture.minFilter = THREE.NearestFilter;
        canvasTexture.format = THREE.RGBAFormat;
        canvasTexture.type = THREE.UnsignedByteType;

        await init();

        async function init() {
            camera = new THREE.PerspectiveCamera();
            camera.fov = 60;
            camera.near = 1;
            camera.far = 2100;
            camera.position.z = 50;

            scene = new THREE.Scene();

            const makeFaceGeometry = (uvs) => {
                const geometry = new THREE.BufferGeometry();

                const positions = [-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, 1, 0];

                geometry.setAttribute(
                    "position",
                    new THREE.BufferAttribute(new Float32Array(positions), 3)
                );

                const indices = [0, 1, 2, 2, 3, 0];

                geometry.setIndex(indices);

                geometry.setAttribute(
                    "uv",
                    new THREE.BufferAttribute(new Float32Array(uvs), 2)
                );

                return geometry;
            };

            const material = new THREE.MeshBasicNodeMaterial();
            material.colorNode = Fn(() => {
                return texture(canvasTexture, uv()).rgb;
            })();

            for (let x = -5; x < 5; x++) {
                for (let y = -5; y < 5; y++) {
                    const face = faces[Math.floor(Math.random() * faces.length)];
                    const geometry = makeFaceGeometry(face);

                    const mesh = new THREE.Mesh(geometry, material);

                    mesh.position.set(x * 2, y * 2, 0);
                    scene.add(mesh);
                }
            }

            rendererAntialisingDisabled = new THREE.WebGPURenderer({
                antialias: false,
            });
            document.body
                .querySelector("#antialising-disabled")
                .appendChild(rendererAntialisingDisabled.domElement);

            rendererAntialisingEnabled = new THREE.WebGPURenderer({
                antialias: true,
            });
            document.body
                .querySelector("#antialising-enabled")
                .appendChild(rendererAntialisingEnabled.domElement);

            await rendererAntialisingEnabled.init();
            await rendererAntialisingDisabled.init();

            new OrbitControls(camera, rendererAntialisingEnabled.domElement);
            new OrbitControls(camera, rendererAntialisingDisabled.domElement);

            window.addEventListener("resize", onWindowResize);
            onWindowResize();
        }

        function onWindowResize() {
            const width = window.innerWidth / 2;
            const height = window.innerHeight;

            camera.aspect = width / height;
            camera.updateProjectionMatrix();

            rendererAntialisingDisabled.setSize(
                window.innerWidth / 2,
                window.innerHeight
            );
            rendererAntialisingDisabled.setPixelRatio(window.devicePixelRatio);

            rendererAntialisingEnabled.setSize(
                window.innerWidth / 2,
                window.innerHeight
            );
            rendererAntialisingEnabled.setPixelRatio(window.devicePixelRatio);
        }

        function loop() {
            requestAnimationFrame(loop);
            rendererAntialisingDisabled.render(scene, camera);
            rendererAntialisingEnabled.render(scene, camera);
        }

        loop();
    </script>
</body>

</html>

@cmhhelgeson
Copy link
Contributor

I was able to get the centroid interpolation working on my bitonic compute. I'll try your sample tomorrow.

@cmhhelgeson
Copy link
Contributor

cmhhelgeson commented Feb 21, 2025

@isaac-mason

Here's a port of your example with a toggle to switch between centroid interpolation sampling and the default interpolation sampling.

[Live WebGPU Example](https://raw.githack.com/cmhhelgeson/three.js/cmh/centroid/examples/webgpu_centroid_sampling.html

I think your Minecraft example would be a good example for the webpage with a larger bounding box and some camera movement, as the parallax of the seams would make the difference between the interpolation more obvious. For now, I'm going to create a branch that simply implements the change without any associated sample.

@isaac-mason
Copy link
Contributor Author

isaac-mason commented Feb 22, 2025

Thanks for working on this @cmhhelgeson!

If a voxel example for this would be useful I'd be happy to create a simplified javascript version as a follow up.

The linked demo has some features that aren't strictly relevant to a more minimal demo, if that's what we want to optimise for (eg voxel ambient occlusion calculation, color and texture block face texture atlas building). If the goal is to have a general voxels example that's all probably fine, but if the goal is to have a minimal demo for this it may be too much.

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

No branches or pull requests

3 participants