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))