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