Strange artifacts when porting GLSL to Metal

I tried to port this shadertoy:

https://www.shadertoy.com/view/4dX3zl

to Metal with as little changes as possible.

But there are little artefacts when I run it on Mac or iPhone: some voxels would sometime "jump" out of place. This happens with both - branchless and ordinary versions.

The original shadertoy runs nicely in a browser without any issues.

I wonder what causes these artefacts and how can I get rid of them...

Here is the Swift+Metal code that you may simply run in a Playground or in Swift Playgrounds app:

import MetalKit
import PlaygroundSupport

public let metalFunctions = """
#include <metal_stdlib>
using namespace metal;

        constant constexpr int MAX_RAY_STEPS = 64;

        float sdSphere(float3 p, float d) {
            return length(p) - d;
        }
        float sdBox(float3 p, float3 b) {
            float3 d = abs(p) - b;
            return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
        }
        bool getVoxel(int3 c) {
            float3 p = float3(c) + float3(0.5);
            float d = min(max(-sdSphere(p, 7.5), sdBox(p, float3(6.0))), -sdSphere(p, 25.0));
            return d < 0.0;
        }
        float2 rotate2d(float2 v, float a) {
            float sinA = sin(a);
            float cosA = cos(a);
            return float2(v.x * cosA - v.y * sinA, v.y * cosA + v.x * sinA);
        }
kernel void shader(texture2d<float, access::write> out [[texture(0)]], constant float& time [[buffer(1)]], constant uint2& viewportSize [[buffer(0)]], uint2 tid [[thread_position_in_grid]]){
    if (tid.x >= viewportSize.x || tid.y >= viewportSize.y) return;
    
    float2 res = float2(viewportSize);
    float2 fragCoord = float2(tid);
    float2 screenPos = (fragCoord / res) * 2.0 - 1.0;
    float3 cameraDir = float3(0.0, 0.0, 0.8);
    float3 cameraPlaneU = float3(1.0, 0.0, 0.0);
    float3 cameraPlaneV = float3(0.0, 1.0, 0.0) * res.y / res.x;
    float3 rayDir = cameraDir + screenPos.x * cameraPlaneU + screenPos.y * cameraPlaneV;
    float3 rayPos = float3(0.0, 2.0 * sin(time * 2.7), -12.0);

    rayPos.xz = rotate2d(rayPos.xz, time);
    rayDir.xz = rotate2d(rayDir.xz, time);

    int3 mapPos = int3(floor(rayPos + 0.5));

    float3 deltaDist = abs(float3(length(rayDir)) / rayDir);
    int3 rayStep = int3(sign(rayDir));
    float3 sideDist = (sign(rayDir) * (float3(mapPos) - rayPos) + (sign(rayDir) * 0.5) + 0.5) * deltaDist;

    bool3 mask;

    for (int i = 0; i < MAX_RAY_STEPS; i++){
        if (getVoxel(mapPos))
            continue;

            mask = sideDist.xyz <= min(sideDist.yzx, sideDist.zxy);
            sideDist += float3(mask) * deltaDist;
            mapPos += int3(float3(mask)) * rayStep; }    

    float3 color;
    if (mask.x) {
        color = float3(0.5);
    }
    if (mask.y) {
        color = float3(1.0);
    }
    if (mask.z) {
        color = float3(0.75);
    }

   // float3 color = .5;
    out.write(float4(color, 1), tid);
}

"""

class MainView: MTKView { 
    
    var time: Float = 0
    
    var viewportSize: simd_uint2 = [0,0]
    
    var commandQueue: MTLCommandQueue!
    var computePass: MTLComputePipelineState!
    
    init(frame:CGRect) {
        
        super.init(frame: frame, device: MTLCreateSystemDefaultDevice())
        
        self.framebufferOnly = false
        self.commandQueue = device?.makeCommandQueue()
        
        var library:MTLLibrary!
        
        do {
            library = try device?.makeLibrary(source: metalFunctions, options: nil)
        } catch{
            print(error)
        }
            
        let shader = library?.makeFunction(name: "shader")
        
        do{
            computePass = try device?.makeComputePipelineState(function: shader!)
        } catch{
            print(error)
        }
        
    }

    required init(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

extension MainView{
    
    override func draw(_ dirtyRect: CGRect) {
        guard let drawable = self.currentDrawable else { return }
        
        viewportSize = [UInt32(drawable.texture.width),
                        UInt32(drawable.texture.height)]
        
        time += 0.01
        
        let commandbuffer = commandQueue.makeCommandBuffer()
        let computeCommandEncoder = commandbuffer?.makeComputeCommandEncoder()
        
        computeCommandEncoder?.setComputePipelineState(computePass)
        computeCommandEncoder?.setTexture(drawable.texture, index: 0)
         computeCommandEncoder?.setBytes(&viewportSize, length: MemoryLayout<simd_uint2>.stride, index: 0)
        computeCommandEncoder?.setBytes(&time, length: MemoryLayout<Float>.stride, index: 1)
        
        var w = computePass.threadExecutionWidth
        let h = computePass.maxTotalThreadsPerThreadgroup / w
        
        var threadsPerThreadGroup = MTLSize(width: w, height: h, depth: 1)
        var threadsPerGrid = MTLSize(width: drawable.texture.width, height: drawable.texture.height, depth: 1)
        var threadgroupsPerGrid = MTLSize(width: drawable.texture.width / w+1, height: drawable.texture.height / h+1, depth: 1)
        computeCommandEncoder?.dispatchThreadgroups(threadgroupsPerGrid, threadsPerThreadgroup: threadsPerThreadGroup)
        
        computeCommandEncoder?.endEncoding()
        commandbuffer?.present(drawable)
        commandbuffer?.commit()
    }
}

let frame = CGRect(x: 0, y: 0, width: 1000, height: 1000)

PlaygroundPage.current.setLiveView(MainView(frame: frame))
Answered by Gadirom in 754468022

The problem was caused by this line: int3 mapPos = int3(floor(rayPos + 0.5));

I used ChatGPT for code conversion and somehow missed that it made this silent addition.

Accepted Answer

The problem was caused by this line: int3 mapPos = int3(floor(rayPos + 0.5));

I used ChatGPT for code conversion and somehow missed that it made this silent addition.

Thank you for posting this! I got your code working in iPad Playgrounds where I’ve been trying to learn metal and it’s a whole new drawing technique for me. I’ve seen hints about drawing this way but never enough to put all the pieces together and having a small, simple example to explore has really helped it click. Before this I’ve only been drawing line and triangle strips with vertex colors so this is very exciting.

Do you know any place with more simple examples of different basic techniques?

Strange artifacts when porting GLSL to Metal
 
 
Q