How to write the texture() function from ShaderToy to Metal shading language?

Viewed 765

I am working on a custom Metal shader, and I am trying to replicate this particular effect from shader toy: https://www.shadertoy.com/view/3sfcR2

But I can't seem to understand how to convert their texture() function to the Metal shader format. Any ideas? Here's what I have so far in Metal:

#include <metal_stdlib>
using namespace metal;

kernel void chromaticAberration(texture2d<float, access::read> inTexture [[ texture(0) ]],
                        texture2d<float, access::write> outTexture [[ texture(1) ]],
                        device const float *time [[ buffer(0) ]],
                        uint2 gid [[ thread_position_in_grid ]])
{
    float ChromaticAberration = 0.0 / 10.0 + 8.0;
    
    // get the width and height of the screen texture
    uint width = outTexture.get_width();
    uint height = outTexture.get_height();
    
    // set its resolution
    float2 iResolution = float2(width, height);
    
    float4 orig = inTexture.read(gid);
    float2 uv = orig.xy / iResolution.xy;
    
    float2 texel = 1.0 / iResolution.xy;
    
    float2 coords = (uv - 0.5) * 2.0;
    float coordDot = dot (coords, coords);
    
    float2 precompute = ChromaticAberration * coordDot * coords;
    float2 uvR = uv - texel.xy * precompute;
    float2 uvB = uv + texel.xy * precompute;
    
    // How to convert these texture() functions?
    float r = texture(iChannel0, uvR).r;
    float g = texture(iChannel0, uv).g;
    float b = texture(iChannel0, uvB).b;
    float a = 1.;
    
    const float4 colorAtPixel = float4(r,g,b,1.0);
    outTexture.write(colorAtPixel, gid);
    
}

EDIT: Following the answer of @JustSomeGuy I was able to successfully replicate this shader in Metal. Here is the final version:

#include <metal_stdlib>
using namespace metal;

kernel void chromaticAberration(texture2d<float, access::read> inTexture [[ texture(0) ]],
                        texture2d<float, access::write> outTexture [[ texture(1) ]],
                        texture2d<float, access::sample> sampleTexture [[ texture(2) ]],
                        device const float *time [[ buffer(0) ]],
                        uint2 gid [[ thread_position_in_grid ]])
{
    
    float ChromaticAberration = 0.0 / 10.0 + 8.0;
    
    // get the width and height of the screen texture
    uint width = inTexture.get_width();
    uint height = inTexture.get_height();
    
    // set its resolution
    float2 iResolution = float2(width, height);
    
    float2 uv = float2(gid) / iResolution.xy;
    
    float2 texel = 1.0 / iResolution.xy;
    
    float2 coords = (uv - 0.5) * 2.0;
    float coordDot = dot (coords, coords);
    
    float2 precompute = ChromaticAberration * coordDot * coords;
    float2 uvR = uv - texel.xy * precompute;
    float2 uvB = uv + texel.xy * precompute;

    constexpr sampler s(address::clamp_to_edge, filter::linear);
    float r = sampleTexture.sample(s, uvR).r;
    float g = sampleTexture.sample(s, uv).g;
    float b = sampleTexture.sample(s, uvB).b;
    
    const float4 colorAtPixel = float4(r,g,b,1.0);
    outTexture.write(colorAtPixel, gid);
}

Kudos to @JustSomeGuy! Thank you for your help!

1 Answers

Well, I think ShaderToy uses glsl or some of it's variants, so texture function is basically a sample call in Metal. Let's look at an example. I'm using this doc. We'll use the 2D version since that's what you probably want.

gvec4 texture(  gsampler2D sampler,
                vec2 P,
                [float bias]);

So in this case iChannel0 is your sampler and uvR, uv, uvB are texture coordinates (P). They should be float2.

So this is a global function that samples color for us from a sampler. In Metal, we have separate textures and samplers and you'll need both to sample. Also, in Metal sample is not a global function, but a member function of a texture2d. Let's look at Metal Language Specification, Section 6.10.3 "2D Texture". There we'll find a method:

Tv sample(sampler s, float2 coord, int2 offset = int2(0)) const

where Tv is the template parameter you have in your texture2d instantiation (probably half or float). It also takes a sampler and texcoords, so this code from your sample:

float r = texture(iChannel0, uvR).r;
float g = texture(iChannel0, uv).g;
float b = texture(iChannel0, uvB).b;

will turn into something like this:

constexpr sampler mySampler { filter::linear };
float r = iChannel0.sample(mySampler, uvR).r;
float g = iChannel0.sample(mySampler, uv).g;
float b = iChannel0.sample(mySampler, uvB).b;

And you will also need to pass texture2d<float> iChannel [[texture(N)]] (where N is the index you chose) to your shader the same way shadertoy does it (it's just a global var there, but in Metal you'd need to actually pass it as an argument).

Related