~connorbell/Physarum-Metal

e569dafd378c149692257a68a59871040d64b9cc — Connor Bell 3 years ago 4bc2d3b
Better organization of shaders
14 files changed, 363 insertions(+), 229 deletions(-)

M Physarum.xcodeproj/project.pbxproj
M Physarum.xcodeproj/project.xcworkspace/xcuserdata/connorbell.xcuserdatad/UserInterfaceState.xcuserstate
M Physarum/Base.lproj/Main.storyboard
R Physarum/{Particle.h => Metal/Includes/Particle.h}
R Physarum/{SimParameters.h => Metal/Includes/SimParameters.h}
R Physarum/{TexturePassParameters.h => Metal/Includes/TexturePassParameters.h}
A Physarum/Metal/Includes/mod.h
A Physarum/Metal/Includes/mod.metal
A Physarum/Metal/Includes/noise.h
R Physarum/{Shaders.metal => Metal/Includes/noise.metal}
A Physarum/Metal/Shaders.metal
A Physarum/Metal/SimulationKernel.metal
A Physarum/Metal/TextureKernel.metal
M Physarum/Simulation.swift
M Physarum.xcodeproj/project.pbxproj => Physarum.xcodeproj/project.pbxproj +43 -7
@@ 14,9 14,13 @@
		B98501882597943D00A03727 /* PhysarumTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B98501872597943D00A03727 /* PhysarumTests.swift */; };
		B98501932597943D00A03727 /* PhysarumUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B98501922597943D00A03727 /* PhysarumUITests.swift */; };
		B98501AA259ADBF100A03727 /* Simulation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B98501A9259ADBF100A03727 /* Simulation.swift */; };
		B98501B9259AE7A100A03727 /* Shaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = B98501B8259AE7A100A03727 /* Shaders.metal */; };
		B98501ED25A0D6EB00A03727 /* MTLTexture+Z.swift in Sources */ = {isa = PBXBuildFile; fileRef = B98501EC25A0D6EA00A03727 /* MTLTexture+Z.swift */; };
		B985020725A2376F00A03727 /* Simulation+SaveFrames.swift in Sources */ = {isa = PBXBuildFile; fileRef = B985020625A2376F00A03727 /* Simulation+SaveFrames.swift */; };
		B9E6211425C10AF500A5683C /* SimulationKernel.metal in Sources */ = {isa = PBXBuildFile; fileRef = B9E6211325C10AF500A5683C /* SimulationKernel.metal */; };
		B9E6211925C10B4900A5683C /* mod.metal in Sources */ = {isa = PBXBuildFile; fileRef = B9E6211825C10B4900A5683C /* mod.metal */; };
		B9E6212525C10C9500A5683C /* noise.metal in Sources */ = {isa = PBXBuildFile; fileRef = B9E6212425C10C9500A5683C /* noise.metal */; };
		B9E6212A25C10D3900A5683C /* TextureKernel.metal in Sources */ = {isa = PBXBuildFile; fileRef = B9E6212925C10D3900A5683C /* TextureKernel.metal */; };
		B9F00E7225C11227009B4E61 /* Shaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = B9F00E7125C11227009B4E61 /* Shaders.metal */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */


@@ 52,12 56,18 @@
		B98501922597943D00A03727 /* PhysarumUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhysarumUITests.swift; sourceTree = "<group>"; };
		B98501942597943D00A03727 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
		B98501A9259ADBF100A03727 /* Simulation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Simulation.swift; sourceTree = "<group>"; };
		B98501B8259AE7A100A03727 /* Shaders.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Shaders.metal; sourceTree = "<group>"; };
		B98501C0259B814F00A03727 /* Particle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Particle.h; sourceTree = "<group>"; };
		B98501C2259B822100A03727 /* Physarum-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Physarum-Bridging-Header.h"; sourceTree = "<group>"; };
		B98501DD259E2ACE00A03727 /* SimParameters.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SimParameters.h; sourceTree = "<group>"; };
		B98501EC25A0D6EA00A03727 /* MTLTexture+Z.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MTLTexture+Z.swift"; sourceTree = "<group>"; };
		B985020625A2376F00A03727 /* Simulation+SaveFrames.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Simulation+SaveFrames.swift"; sourceTree = "<group>"; };
		B9E6211325C10AF500A5683C /* SimulationKernel.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = SimulationKernel.metal; sourceTree = "<group>"; };
		B9E6211825C10B4900A5683C /* mod.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = mod.metal; sourceTree = "<group>"; };
		B9E6211D25C10B5D00A5683C /* mod.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = mod.h; sourceTree = "<group>"; };
		B9E6212325C10C8B00A5683C /* noise.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = noise.h; sourceTree = "<group>"; };
		B9E6212425C10C9500A5683C /* noise.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = noise.metal; sourceTree = "<group>"; };
		B9E6212925C10D3900A5683C /* TextureKernel.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = TextureKernel.metal; sourceTree = "<group>"; };
		B9F00E7125C11227009B4E61 /* Shaders.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = Shaders.metal; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */


@@ 113,11 123,8 @@
				B98501762597943A00A03727 /* ViewController.swift */,
				B98501A9259ADBF100A03727 /* Simulation.swift */,
				B985020625A2376F00A03727 /* Simulation+SaveFrames.swift */,
				B98501DD259E2ACE00A03727 /* SimParameters.h */,
				B90CC8D425A904E90079ED0E /* TexturePassParameters.h */,
				B98501C0259B814F00A03727 /* Particle.h */,
				B98501C2259B822100A03727 /* Physarum-Bridging-Header.h */,
				B98501B8259AE7A100A03727 /* Shaders.metal */,
				B9E6211E25C10C6300A5683C /* Metal */,
				B98501782597943D00A03727 /* Assets.xcassets */,
				B985017A2597943D00A03727 /* Main.storyboard */,
				B985017D2597943D00A03727 /* Info.plist */,


@@ 144,6 151,31 @@
			path = PhysarumUITests;
			sourceTree = "<group>";
		};
		B9E6211E25C10C6300A5683C /* Metal */ = {
			isa = PBXGroup;
			children = (
				B9F00E7125C11227009B4E61 /* Shaders.metal */,
				B9E6212925C10D3900A5683C /* TextureKernel.metal */,
				B9E6211325C10AF500A5683C /* SimulationKernel.metal */,
				B9E6211F25C10C6C00A5683C /* Includes */,
			);
			path = Metal;
			sourceTree = "<group>";
		};
		B9E6211F25C10C6C00A5683C /* Includes */ = {
			isa = PBXGroup;
			children = (
				B98501DD259E2ACE00A03727 /* SimParameters.h */,
				B98501C0259B814F00A03727 /* Particle.h */,
				B90CC8D425A904E90079ED0E /* TexturePassParameters.h */,
				B9E6211825C10B4900A5683C /* mod.metal */,
				B9E6211D25C10B5D00A5683C /* mod.h */,
				B9E6212325C10C8B00A5683C /* noise.h */,
				B9E6212425C10C9500A5683C /* noise.metal */,
			);
			path = Includes;
			sourceTree = "<group>";
		};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */


@@ 273,11 305,15 @@
			isa = PBXSourcesBuildPhase;
			buildActionMask = 2147483647;
			files = (
				B98501B9259AE7A100A03727 /* Shaders.metal in Sources */,
				B9E6212A25C10D3900A5683C /* TextureKernel.metal in Sources */,
				B9E6211425C10AF500A5683C /* SimulationKernel.metal in Sources */,
				B9E6211925C10B4900A5683C /* mod.metal in Sources */,
				B985020725A2376F00A03727 /* Simulation+SaveFrames.swift in Sources */,
				B9F00E7225C11227009B4E61 /* Shaders.metal in Sources */,
				B98501ED25A0D6EB00A03727 /* MTLTexture+Z.swift in Sources */,
				B98501772597943A00A03727 /* ViewController.swift in Sources */,
				B98501AA259ADBF100A03727 /* Simulation.swift in Sources */,
				B9E6212525C10C9500A5683C /* noise.metal in Sources */,
				B98501752597943A00A03727 /* AppDelegate.swift in Sources */,
			);
			runOnlyForDeploymentPostprocessing = 0;

M Physarum.xcodeproj/project.xcworkspace/xcuserdata/connorbell.xcuserdatad/UserInterfaceState.xcuserstate => Physarum.xcodeproj/project.xcworkspace/xcuserdata/connorbell.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
M Physarum/Base.lproj/Main.storyboard => Physarum/Base.lproj/Main.storyboard +3 -3
@@ 707,11 707,11 @@
            <objects>
                <viewController id="XfG-lQ-9wD" customClass="ViewController" customModule="Physarum" customModuleProvider="target" sceneMemberID="viewController">
                    <view key="view" id="m2S-Jp-Qdl">
                        <rect key="frame" x="0.0" y="0.0" width="750" height="750"/>
                        <rect key="frame" x="0.0" y="0.0" width="500" height="500"/>
                        <autoresizingMask key="autoresizingMask"/>
                        <subviews>
                            <button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Kk9-jN-RMZ">
                                <rect key="frame" x="697" y="19" width="33" height="23"/>
                                <rect key="frame" x="447" y="19" width="33" height="23"/>
                                <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
                                <buttonCell key="cell" type="roundTextured" title="🛠" bezelStyle="texturedRounded" alignment="center" lineBreakMode="truncatingTail" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="ZGR-XZ-fxz">
                                    <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>


@@ 719,7 719,7 @@
                                </buttonCell>
                            </button>
                            <scrollView fixedFrame="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iLG-dn-t8y">
                                <rect key="frame" x="448" y="20" width="282" height="172"/>
                                <rect key="frame" x="198" y="20" width="282" height="172"/>
                                <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
                                <clipView key="contentView" id="uUa-9T-fcw">
                                    <rect key="frame" x="1" y="1" width="280" height="170"/>

R Physarum/Particle.h => Physarum/Metal/Includes/Particle.h +1 -0
@@ 7,6 7,7 @@ typedef struct {
    float sensorHeading;    
    vector_float2 position;
    int active;
    vector_float3 color;
} Particle;

#endif

R Physarum/SimParameters.h => Physarum/Metal/Includes/SimParameters.h +0 -0
R Physarum/TexturePassParameters.h => Physarum/Metal/Includes/TexturePassParameters.h +0 -0
A Physarum/Metal/Includes/mod.h => Physarum/Metal/Includes/mod.h +16 -0
@@ 0,0 1,16 @@
//
//  mod.h
//  Physarum
//
//  Created by Connor Bell on 2021-01-26.
//

#ifndef mod_h
#define mod_h

float mod(float x, float y);
float2 mod(float2 x, float2 y);
float3 mod(float3 x, float3 y);
float4 mod(float4 x, float4 y);

#endif /* mod_h */

A Physarum/Metal/Includes/mod.metal => Physarum/Metal/Includes/mod.metal +30 -0
@@ 0,0 1,30 @@
//
//  mod.metal
//  Physarum
//
//  Created by Connor Bell on 2021-01-26.
//

#include <metal_stdlib>
using namespace metal;

// glsl mod <3
float2 mod(float2 x, float2 y)
{
    return x - y * floor(x/y);
}

float mod(float x, float y)
{
    return x - y * floor(x/y);
}

float3 mod(float3 x, float3 y)
{
    return x - y * floor(x/y);
}

float4 mod(float4 x, float4 y)
{
    return x - y * floor(x/y);
}

A Physarum/Metal/Includes/noise.h => Physarum/Metal/Includes/noise.h +13 -0
@@ 0,0 1,13 @@
//
//  noise.h
//  Physarum
//
//  Created by Connor Bell on 2021-01-26.
//

#ifndef noise_h
#define noise_h

float3 curl(float3 pos);

#endif /* noise_h */

R Physarum/Shaders.metal => Physarum/Metal/Includes/noise.metal +3 -208
@@ 1,48 1,15 @@
//
//  Shaders.metal
//  noise.metal
//  Physarum
//
//  Created by Connor Bell on 2020-12-28.
//  Created by Connor Bell on 2021-01-26.
//

#include <metal_stdlib>
#include "Particle.h"
#include "SimParameters.h"
#include "TexturePassParameters.h"
#include "mod.h"

using namespace metal;

float random(float n) {
    return fract(sin(n) * 43758.5453123);
}

// glsl mod <3
float2 mod(float2 x, float2 y)
{
    return x - y * floor(x/y);
}

float mod(float x, float y)
{
    return x - y * floor(x/y);
}

float3 mod(float3 x, float3 y)
{
    return x - y * floor(x/y);
}

float4 mod(float4 x, float4 y)
{
    return x - y * floor(x/y);
}

float3 hueShift(float3 color, float hue) {
    const float3 k = float3(0.57735, 0.57735, 0.57735);
    float cosAngle = cos(hue);
    return float3(color * cosAngle + cross(k, color) * sin(hue) + k * dot(k, color) * (1.0 - cosAngle));
}

float4 permute(float4 x){return mod(((x*34.0)+1.0)*x, 289.0);}
float4 taylorInvSqrt(float4 r){return 1.79284291400159 - 0.85373472095314 * r;}



@@ 144,175 111,3 @@ float3 curl(float3 pos) {

    return res;
}

float3 hsv2rgb(float3 c)
{
    const float4 K = float4(1.0, 0.66667, 0.33333, 3.0);
    float3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

kernel void setupParticles(device Particle *particles [[buffer(0)]],
                           device const int &width [[ buffer(1) ]],
                           device const int &height [[ buffer(2) ]],
                           uint id [[ thread_position_in_grid ]])

{
    Particle p = particles[id];
    p.active = 0;
    p.position = float2(width/2.0,height/2.0) + (float2(cos(id*0.00235), sin(id*0.0059022))*(250+100));

    p.sensorHeading = random(id*0.034) * 6.28318;

    particles[id] = p;
}

kernel void addParticles(device Particle *particles [[buffer(0)]],
                         device const int &width [[ buffer(1) ]],
                         device const int &height [[ buffer(2) ]],
                         device const float &startupProgress [[ buffer(3) ]],
                         device const int &offset [[ buffer(4) ]],
                         uint id [[ thread_position_in_grid ]])
{
    uint index = id + offset;
    Particle particle = particles[index];
    particle.active = 1;
    float halfWidth = width / 2.0;
    float startX = width/4.0;
    float2 pos = float2(halfWidth + cos(-.5+startupProgress * 3.14159 * 3.5) * halfWidth * (1.-startupProgress) * 0.75,
                        height/2.0 + sin(-.5+startupProgress * 3.14159 * 3.5) * halfWidth* (1.-startupProgress)  * 0.75);
    
    particle.position = pos;
    particle.sensorHeading = -.5 + 3.14159 + startupProgress*3.14159*3.5 + random(id*0.104) ;
    particles[index] = particle;
}


kernel void updateParticles(device Particle *particles [[buffer(0)]],
                            device const float &time [[ buffer(1) ]],
                            device SimParameters *parameters [[buffer(2)]],
                            texture2d<float, access::read> inTexture [[texture(0)]],
                            texture2d<float, access::write> outTexture [[texture(1)]],
                            texture2d<float, access::sample> inTrailMapTexture [[texture(2)]],
                            texture2d<float, access::write> outTrailMapTexture [[texture(3)]],
                            uint id [[ thread_position_in_grid ]])
{
    float2 viewSize = float2(inTexture.get_width(), inTexture.get_height());
    
    // Create a copy of the current particle
    Particle p = particles[id];
    
    if (p.active == 0) return;
    
    uint2 texCoord = uint2(p.position);
    
    float sensorAngle = parameters->sensorAngle;
    // Create coordinates for the sensors to sample the trail map
    float2 coordForward = mod(p.position.xy + float2(cos(p.sensorHeading), sin(p.sensorHeading))*parameters->sensorDistance, viewSize);
    float cA = p.sensorHeading + sensorAngle;
    float2 coordClockwise = mod(p.position.xy + float2(cos(cA), sin(cA))*parameters->sensorDistance, viewSize);
    float ccA = p.sensorHeading - sensorAngle;
    float2 coordCounterClockwise = mod(p.position.xy + float2(cos(ccA), sin(ccA))*parameters->sensorDistance, viewSize);
    
    float trailMapSampleForward = inTrailMapTexture.read((uint2)round(coordForward)).r;
    float trailMapSampleClockwise = inTrailMapTexture.read((uint2)round(coordClockwise)).r;
    float trailMapSampleCounterClockwise = inTrailMapTexture.read((uint2)round(coordCounterClockwise)).r;
    float4 color = mix(float4(0., 0., 0., 1.0), float4(1., 1., 1., 1.0), saturate(time*0.1));

    // Do nothing and move forward if most concentrated forward
    if (trailMapSampleForward > trailMapSampleClockwise && trailMapSampleForward > trailMapSampleCounterClockwise) {

    }
    // Rotate Randomly if not conentrated in center
    else if (trailMapSampleClockwise > trailMapSampleForward && trailMapSampleCounterClockwise > trailMapSampleForward) {
        p.sensorHeading += random(id + time) > 0.5 ? parameters->rotationAngle : -parameters->rotationAngle;
    }
    // Rotate clockwise
    else if (trailMapSampleCounterClockwise < trailMapSampleClockwise) {
        p.sensorHeading += parameters->rotationAngle;
    }
    // Rotate counter clockwise
    else if (trailMapSampleCounterClockwise > trailMapSampleClockwise) {
        p.sensorHeading -= parameters->rotationAngle;
    }
    
    // Write particle color
    outTexture.write(color, texCoord);
    outTexture.write(color, clamp((uint2)(int2(texCoord) + int2(0,1)), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp((uint2)(int2(texCoord) + int2(1,1)), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp((uint2)(int2(texCoord) + int2(1,0)), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp((uint2)(int2(texCoord) + int2(1,-1)), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp((uint2)(int2(texCoord) + int2(0,-1)), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp((uint2)(int2(texCoord) + int2(-1,-1)), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp((uint2)(int2(texCoord) + int2(-1,0)), uint2(0.), uint2(viewSize)));
    outTexture.write(color, clamp((uint2)(int2(texCoord) + int2(-1,1)), uint2(0.), uint2(viewSize)));
    
    // Deposit
    float trailMapSample = inTrailMapTexture.read(texCoord).r;
    trailMapSample = 1.0;
    outTrailMapTexture.write(float4(trailMapSample, 1., 0., 1.0), texCoord);
    
    // Move
    p.position.xy = mod(p.position.xy + float2(cos(p.sensorHeading), sin(p.sensorHeading))*(parameters->movementOffset +  random(id*520.421235) * parameters->movementOffsetRandomness), viewSize);

    particles[id] = p;
}

// Perform a box blur on both the trail and color texture
kernel void texturePass(texture2d<float, access::sample> inTexture [[texture(0)]],
                        texture2d<float, access::write> outTexture [[texture(1)]],
                        texture2d<float, access::sample> inTrailTexture [[texture(2)]],
                        texture2d<float, access::write> outTrailTexture [[texture(3)]],
                        device TexturePassParameters *parameters [[buffer(0)]],
                        device const float &time [[buffer(1)]],
                        uint2 gid [[ thread_position_in_grid ]]) {

    constexpr sampler colorSampler(coord::normalized,
                                   address::repeat,
                                   filter::linear);
    
    // Compute weighted sum of surrounding box
    float3 col = float3(0.);
    float3 colTrail = float3(0.);
    float2 size = float2(inTexture.get_width(), inTexture.get_height());
    float2 texel = 1. / size;
    float2 uv = (float2)gid / size;
    
    // Non uniform weights to created colored feedback.
    float3 weight = float3(0.07, 0.1111111111, 0.1);

    for (int i = -1; i <= 1; i++) {
        for (int j = -1; j <= 1; j++) {
            float3 val = inTexture.read((uint2)(int2(gid) + int2(i,j))).rgb;
            col += val * weight;
            
            val = inTrailTexture.read((uint2)(int2(gid) + int2(i,j))).rgb;
            colTrail += val * 0.111111;
        }
    }

    // Add trail for mouse position
    if (length(uv - parameters->mousePosition) < 0.05) {
        colTrail = float3(1.0, 0.0, 0.0);
    }
    
    // Dampen
    col.rgb *= parameters->additiveBlendFrameFactor;
    colTrail *= parameters->additiveBlendTrailFactor;
    
    col.rgb = hueShift(col.rgb, 0.175);
    colTrail = clamp(colTrail, float3(0.01), float3(1.0));

    outTexture.write(float4(col, 1.0), gid);
    outTrailTexture.write(float4(colTrail, 1.0), gid);
}


kernel void postFx(texture2d<float, access::sample> inTexture [[texture(0)]],
                   texture2d<float, access::write> outTexture [[texture(1)]],
                   uint2 gid [[ thread_position_in_grid ]]) {

    float4 col = inTexture.read(gid);
    col.rgb = 1.0 - col.rgb;
    outTexture.write(col, gid);
}

A Physarum/Metal/Shaders.metal => Physarum/Metal/Shaders.metal +72 -0
@@ 0,0 1,72 @@
//
//  Shaders.metal
//  Physarum
//
//  Created by Connor Bell on 2020-12-28.
//

#include <metal_stdlib>
#include "Includes/Particle.h"
#include "Includes/SimParameters.h"
#include "Includes/TexturePassParameters.h"

using namespace metal;

float randomOk(float n) {
    return fract(sin(n) * 43758.5453123);
}

float3 hueShift(float3 color, float hue) {
    const float3 k = float3(0.57735, 0.57735, 0.57735);
    float cosAngle = cos(hue);
    return float3(color * cosAngle + cross(k, color) * sin(hue) + k * dot(k, color) * (1.0 - cosAngle));
}

kernel void setupParticles(device Particle *particles [[buffer(0)]],
                           device const int &width [[ buffer(1) ]],
                           device const int &height [[ buffer(2) ]],
                           uint id [[ thread_position_in_grid ]])

{
    Particle p = particles[id];
    p.active = 0;
    p.position = float2(width/2.0,height/2.0) + (float2(cos(id*0.00235), sin(id*0.0059022))*(250+100));

    p.sensorHeading = randomOk(id*0.034) * 6.28318;

    particles[id] = p;
}

kernel void addParticles(device Particle *particles [[buffer(0)]],
                         device const int &width [[ buffer(1) ]],
                         device const int &height [[ buffer(2) ]],
                         device const float &startupProgress [[ buffer(3) ]],
                         device const int &offset [[ buffer(4) ]],
                         uint id [[ thread_position_in_grid ]])
{
    uint index = id + offset;
    Particle particle = particles[index];
    particle.active = 1;
    float halfWidth = width / 2.0;
    float halfHeight = height / 2.0;
    float startX = width/4.0;
    float angle = startupProgress*3.14159*2;
    float2 pos = float2(halfWidth + cos(angle)*5*startupProgress, halfHeight + sin(angle)*5*startupProgress);
    
    if (index % 2 == 0) {
        pos.y = - pos.y;
    }
    particle.position = pos;
    
    particle.sensorHeading = -1.5 + 3.14159 + startupProgress*3.14159*3.5 + randomOk(id*0.104) ;
    particles[index] = particle;
}

kernel void postFx(texture2d<float, access::sample> inTexture [[texture(0)]],
                   texture2d<float, access::write> outTexture [[texture(1)]],
                   uint2 gid [[ thread_position_in_grid ]]) {

    float4 col = inTexture.read(gid);
    col.rgb = 1.-col.rgb;
    outTexture.write(col, gid);
}

A Physarum/Metal/SimulationKernel.metal => Physarum/Metal/SimulationKernel.metal +93 -0
@@ 0,0 1,93 @@
//
//  SimulationKernel.metal
//  Physarum
//
//  Created by Connor Bell on 2021-01-26.
//

#include <metal_stdlib>

#include "Includes/Particle.h"
#include "Includes/SimParameters.h"
#include "Includes/mod.h"

using namespace metal;

float random(float n) {
    return fract(sin(n) * 3629135.423193);
}

// Write a 9x9 pixel area around the grid id with color, blended with the previous color
void writePixelWithBlendedColor(float4 color,
                                  uint2 gid,
                                  texture2d<float, access::read> inTexture,
                                  texture2d<float, access::write> outTexture,
                                  uint2 viewSize) {
    
    for (int i = -1; i <= 1; i++) {
        for (int j = -1; j <= 1; j++) {
            outTexture.write(color, clamp((uint2)(int2(gid) + int2(0,1)), uint2(0.), viewSize));
        }
    }
}

kernel void updateParticles(device Particle *particles [[buffer(0)]],
                            device const float &time [[ buffer(1) ]],
                            device SimParameters *parameters [[buffer(2)]],
                            texture2d<float, access::read> inTexture [[texture(0)]],
                            texture2d<float, access::write> outTexture [[texture(1)]],
                            texture2d<float, access::sample> inTrailMapTexture [[texture(2)]],
                            texture2d<float, access::write> outTrailMapTexture [[texture(3)]],
                            uint id [[ thread_position_in_grid ]])
{
    float2 viewSize = float2(inTexture.get_width(), inTexture.get_height());
    
    // Create a copy of the current particle
    Particle p = particles[id];
    
    if (p.active == 0) return;
    
    uint2 texCoord = uint2(p.position);
    
    float sensorAngle = parameters->sensorAngle;
    // Create coordinates for the sensors to sample the trail map
    float2 coordForward = mod(p.position.xy + float2(cos(p.sensorHeading), sin(p.sensorHeading))*parameters->sensorDistance, viewSize);
    float cA = p.sensorHeading + sensorAngle;
    float2 coordClockwise = mod(p.position.xy + float2(cos(cA), sin(cA))*parameters->sensorDistance, viewSize);
    float ccA = p.sensorHeading - sensorAngle;
    float2 coordCounterClockwise = mod(p.position.xy + float2(cos(ccA), sin(ccA))*parameters->sensorDistance, viewSize);
    
    float trailMapSampleForward = inTrailMapTexture.read((uint2)round(coordForward)).r;
    float trailMapSampleClockwise = inTrailMapTexture.read((uint2)round(coordClockwise)).r;
    float trailMapSampleCounterClockwise = inTrailMapTexture.read((uint2)round(coordCounterClockwise)).r;
    float4 color = mix(float4(0., 0., 0., 1.0), float4(1.0), saturate(time*0.1));

    // Do nothing and move forward if most concentrated forward
    if (trailMapSampleForward > trailMapSampleClockwise && trailMapSampleForward > trailMapSampleCounterClockwise) {

    }
    // Rotate Randomly if not conentrated in center
    else if (trailMapSampleClockwise > trailMapSampleForward && trailMapSampleCounterClockwise > trailMapSampleForward) {
        p.sensorHeading += random(id + time) > 0.5 ? parameters->rotationAngle : -parameters->rotationAngle;
    }
    // Rotate clockwise
    else if (trailMapSampleCounterClockwise < trailMapSampleClockwise) {
        p.sensorHeading += parameters->rotationAngle;
    }
    // Rotate counter clockwise
    else if (trailMapSampleCounterClockwise > trailMapSampleClockwise) {
        p.sensorHeading -= parameters->rotationAngle;
    }
        
    writePixelWithBlendedColor(color, texCoord, inTexture, outTexture, uint2(viewSize));
    
    // Deposit
    float trailMapSample = inTrailMapTexture.read(texCoord).r;
    trailMapSample = 1.0;
    outTrailMapTexture.write(float4(trailMapSample, 1., 0., 1.0), texCoord);

    // Move
    p.position.xy = mod(p.position.xy + float2(cos(p.sensorHeading), sin(p.sensorHeading))*(parameters->movementOffset + random(id*520.421235) * parameters->movementOffsetRandomness), viewSize);

    particles[id] = p;
}

A Physarum/Metal/TextureKernel.metal => Physarum/Metal/TextureKernel.metal +81 -0
@@ 0,0 1,81 @@
//
//  TextureKernel.metal
//  Physarum
//
//  Created by Connor Bell on 2021-01-26.
//

#include <metal_stdlib>

#include "Includes/TexturePassParameters.h"
#include "Includes/noise.h"

using namespace metal;

// via Inigo Quilez https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm
float sdEquilateralTriangle( float2 p, float r )
{
    const float k = sqrt(3.0);
    p.y = - p.y;
    p.x = abs(p.x) - r;
    p.y = p.y + r/k;
    if( p.x+k*p.y>0.0 ) p=float2(p.x-k*p.y,-k*p.x-p.y)/2.0;
    p.x -= clamp( p.x, -2.0*r, 0.0 );
    return -length(p)*sign(p.y);
}

// Perform a box blur on both the trail and color texture
kernel void texturePass(texture2d<float, access::sample> inTexture [[texture(0)]],
                        texture2d<float, access::write> outTexture [[texture(1)]],
                        texture2d<float, access::sample> inTrailTexture [[texture(2)]],
                        texture2d<float, access::write> outTrailTexture [[texture(3)]],
                        device TexturePassParameters *parameters [[buffer(0)]],
                        device const float &time [[buffer(1)]],
                        uint2 gid [[ thread_position_in_grid ]]) {

    constexpr sampler colorSampler(coord::normalized,
                                   address::repeat,
                                   filter::linear);
    
    // Compute weighted sum of surrounding box
    float3 col = float3(0.);
    float3 colTrail = float3(0.);
    float2 size = float2(inTexture.get_width(), inTexture.get_height());
    float2 texel = 1. / size;
    float2 uv = (float2)gid / size;
    
    // Non uniform weights to created colored feedback.
    float3 weight = float3(0.95, 0.05, 0.1111111111);
    
    for (int i = -1; i <= 1; i++) {
        for (int j = -1; j <= 1; j++) {
            float3 val = inTexture.read((uint2)(int2(gid) + int2(i,j))).rgb;
            col += val * 0.1111111;
            
            val = inTrailTexture.read((uint2)(int2(gid) + int2(i,j))).rgb;
            colTrail += val * 0.111111;
        }
    }
    col *= float3(0.775, 0.95, 1.0);
    // Add trail for mouse position
    if (length(uv - parameters->mousePosition) < 0.05) {
        colTrail = float3(1.0, 0.0, 0.0);
    }
    
    // Dampen
    col.rgb *= parameters->additiveBlendFrameFactor;
    colTrail *= parameters->additiveBlendTrailFactor;
    
    float3 n = curl(float3(uv*15., time*0.05));
    float tri = sdEquilateralTriangle(uv + (n.xy-0.5)*0.015 - float2(0.5, 0.6), 0.4);
    tri = smoothstep(0.035, .0, abs(tri)) * (abs(n.z)*0.25);
    
    //col.rgb = hueShift(col.rgb, 0.15);
    colTrail = clamp(colTrail + tri, float3(0.0), float3(1.0));

    //outTexture.write(float4(float3(tri), 1.0), gid);
    outTexture.write(float4(col, 1.0), gid);
    
    outTrailTexture.write(float4(colTrail, 1.0), gid);
}


M Physarum/Simulation.swift => Physarum/Simulation.swift +8 -11
@@ 15,7 15,7 @@ class Simulation: MTKView {
    // Parameters 
    var simulationParameters: SimParameters
    var texturePassParameters: TexturePassParameters
    var particleCount = 1024*64
    var particleCount = 1024*4
    var activeParticles = 0
    var particlesPerFrame = 1024/4
    


@@ 51,11 51,12 @@ class Simulation: MTKView {
    private var mouseDown = false
    
    // Render PNG properties
    private var numRenderFrames: Int = 1250
    private var numRenderFrames: Int = 850
    private var currentRenderFrame = 0
    var renderFrames: Bool = false
    
    init(width: Int, height: Int) {
        
        self.metalDevice = MTLCreateSystemDefaultDevice()!
        self.commandQueue = metalDevice.makeCommandQueue()!
        self.metalLibrary = metalDevice.makeDefaultLibrary()!


@@ 82,15 83,15 @@ class Simulation: MTKView {
            fatalError("newComputePipelineStateWithFunction failed ")
        }
        let rect = CGRect(x: 0, y: 0, width: width, height: height)
        
        self.simulationParameters = SimParameters(sensorAngle: 0.15,
    
        self.simulationParameters = SimParameters(sensorAngle: 0.125,
                                                  sensorDistance: 8.0,
                                                  movementOffset: 1.1,
                                                  movementOffsetRandomness: 1.0,
                                                  rotationAngle: 0.5)
        
        self.texturePassParameters = TexturePassParameters(additiveBlendFrameFactor: 0.975,
                                                           additiveBlendTrailFactor: 0.9, mousePosition: vector2(-1.0, -1.0))
        self.texturePassParameters = TexturePassParameters(additiveBlendFrameFactor: 0.985,
                                                           additiveBlendTrailFactor: 0.925, mousePosition: vector2(-1.0, -1.0))
        
        super.init(frame: rect,
                   device: self.metalDevice)


@@ 163,12 164,8 @@ extension Simulation {
        
        if activeParticles < particleCount {
            activeParticles += addParticles(commandEncoder: commandEncoder)
            if activeParticles >= particleCount {
                self.simulationParameters.movementOffset += 0.5;
                self.simulationParameters.sensorAngle -= 0.035;
            }
        }
        

        simulate(commandEncoder: commandEncoder)

        dampenAndDecayTextures(drawableIn: renderTextureSwap,