I've been trying to get motion blur working with SCNTechnique for a few days now and I'm nowhere close to what I want. I've asked a similar question on the Apple forums but they're dead. So I'd thought I'd write a more exhaustive description of what I'm trying to achieve with code.
The setup
I have a character on screen with some enemies. The character is a square, the enemies are circles - simplified for this example.
I am using SceneKit with Metal. The camera is fixed.
The Goal
When the circles/enemies move I want them to have motion blur. When the character moves he should not.
Proposed Idea
Write a multipass SCNTechnique that can handle this, I'm assuming that this is a way of doing what I want.
Pass one: Render just the enemies/circles to another buffer
Pass two: Apply motion blur to this buffer
Pass three: Render the motion blur buffer with the original scene.
Side note: Pass two will be tricky as I imagine each object has it's own direction and blur would need to follow suit.
So, here's how I setup the objects in SceneKit
class Character : SCNNode
{
override init() {
super.init()
let img = UIImage(named: "texture1")!
material = SCNMaterial()
material.diffuse.contents = img
material.ambient.contents = img
let geometry = SCNSphere(radius: 40)
geometry.materials = [material]
self.categoryBitMask = 1
}
}
class Enemy : SCNNode
{
override init() {
super.init()
let img = UIImage(named: "texture2")!
material = SCNMaterial()
material.diffuse.contents = img
material.ambient.contents = img
let geometry = SCNSphere(radius: 40)
geometry.materials = [material]
self.categoryBitMask = 2
}
}
I can add these to the scene and they look fine. I can move them both with SCNActions and they move correctly.
Now for how I'm attempting motion blur
First Pass
I want to extract just enemies for this pass so this part of the technique looks like the following, note the category mask draws just the enemies for the scene.
It outputs this to my custom "enemiesColor" target buffer. Note: DRAW_SCENE
<key>drawEnemies</key>
<dict>
<key>draw</key>
<string>DRAW_SCENE</string>
<key>includeCategoryMask</key>
<integer>2</integer>
<key>excludeCategoryMask</key>
<integer>1</integer>
<key>program</key>
<string>doesntexist</string>
<key>metalVertexShader</key>
<string>multi_vertex</string>
<key>metalFragmentShader</key>
<string>multi_fragment_vert</string>
<key>inputs</key>
<dict>
<key>colorSampler</key>
<string>COLOR</string>
<key>a_texcoord</key>
<string>a_texcoord-symbol</string>
<key>aPos</key>
<string>vertexSymbol</string>
</dict>
<key>outputs</key>
<dict>
<key>color</key>
<string>enemiesColor</string>
</dict>
</dict>
The metal shader for this is:
#include <metal_stdlib>
using namespace metal;
#include <SceneKit/scn_metal>
struct custom_node_t3 {
float4x4 modelTransform;
float4x4 modelViewTransform;
float4x4 normalTransform;
float4x4 modelViewProjectionTransform;
};
struct custom_vertex_t
{
float4 position [[attribute(SCNVertexSemanticPosition)]];
float2 a_texcoord [[ attribute(SCNVertexSemanticTexcoord0) ]];
//SCNGeometrySourceSemanticTexcoord
};
constexpr sampler s = sampler(coord::normalized,
address::repeat,
filter::linear);
struct out_vertex_t
{
float4 position [[position]];
float2 texcoord;
};
vertex out_vertex_t multi_vertex(custom_vertex_t in [[stage_in]],
constant custom_node_t3& scn_node [[buffer(0)]])
{
out_vertex_t out;
out.texcoord = in.a_texcoord;
out.position = scn_node.modelViewProjectionTransform * float4(in.position.xyz, 1.0);
return out;
};
fragment half4 multi_fragment_vert(out_vertex_t vert [[stage_in]],
constant SCNSceneBuffer& scn_frame [[buffer(0)]],
texture2d<float, access::sample> colorSampler [[texture(0)]])
{
float4 FragmentColor = colorSampler.sample( s, vert.texcoord);
return half4(FragmentColor);
};
Second Pass
I want to blur the "enemiesColor" buffer, at the moment it's very rough, so I'm just using a gaussian hack at the moment.
I take in the "enemiesColor" buffer as an input and blur it, I output this as a new buffer: "enemyColor" Note: DRAW_QUAD
The technique for this pass looks like:
<key>blurEnemies</key>
<dict>
<key>draw</key>
<string>DRAW_QUAD</string>
<key>program</key>
<string>doesntexist</string>
<key>metalVertexShader</key>
<string>blur_vertex</string>
<key>metalFragmentShader</key>
<string>blur_fragment_vert</string>
<key>inputs</key>
<dict>
<key>colorSampler</key>
<string>COLOR</string>
<key>enemyColor</key>
<string>enemiesColor</string>
<key>a_texcoord</key>
<string>a_texcoord-symbol</string>
</dict>
<key>outputs</key>
<dict>
<key>color</key>
<string>chrisColor</string>
</dict>
</dict>
and the shader is:
// http://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/
constant float offset[] = { 0.0, 1.0, 2.0, 3.0, 4.0 };
constant float weight[] = { 0.2270270270, 0.1945945946, 0.1216216216, 0.0540540541, 0.0162162162 };
vertex out_vertex_t blur_vertex(custom_vertex_t in [[stage_in]],
constant custom_node_t3& scn_node [[buffer(0)]])
{
out_vertex_t out;
out.position = in.position;
out.texcoord = float2((in.position.x + 1.0) * 0.5 , (in.position.y + 1.0) * -0.5);
return out;
};
fragment half4 blur_fragment_vert(out_vertex_t vert [[stage_in]],
texture2d<float, access::sample> colorSampler [[texture(0)]],
texture2d<float, access::sample> enemyColor [[texture(1)]])
{
float4 enemySample = enemyColor.sample(s, vert.texcoord);
if (enemySample.a == 0)
{
//gl_LastFragData[0]
return half4(0.0 ,1.0 ,0.0, 0.5);
}
float4 FragmentColor = colorSampler.sample( s, vert.texcoord) * weight[0];
for (int i=1; i<5; i++) {
FragmentColor += colorSampler.sample( s, ( vert.texcoord + float2(0.0, offset[i])/224.0 ) ) * weight[i];
FragmentColor += colorSampler.sample( s, ( vert.texcoord - float2(0.0, offset[i])/224.0 ) ) * weight[i];
}
return half4(FragmentColor);
};
Third pass
Where things get even messier, I want to then apply the blurred "enemyColor" buffer with the original scene.
My first thought was, well why can't I just overlay the result. I looked into blend modes but found no luck.
Then I thought, maybe I can just re-render the scene and add the "enemyColor" and the new "color" buffer together (there's probably optimisations somewhere if this even remotely works)
Note: DRAW_SCENE
So the technique is:
<key>blendTogether</key>
<dict>
<key>draw</key>
<string>DRAW_SCENE</string>
<key>program</key>
<string>doesntexist</string>
<key>metalVertexShader</key>
<string>plain_vertex</string>
<key>metalFragmentShader</key>
<string>plain_fragment_vert</string>
<key>inputs</key>
<dict>
<key>colorSampler</key>
<string>COLOR</string>
<key>aPos</key>
<string>vertexSymbol</string>
<key>a_texcoord</key>
<string>a_texcoord-symbol</string>
</dict>
<key>outputs</key>
<dict>
<key>color</key>
<string>COLOR</string>
</dict>
</dict>
and the shader:
vertex out_vertex_t plain_vertex(custom_vertex_t in [[stage_in]],
constant SCNSceneBuffer& scn_frame [[buffer(0)]],
constant custom_node_t3& scn_node [[buffer(1)]])
{
out_vertex_t out;
out.position = scn_node.modelViewProjectionTransform * float4(in.position.xyz, 1.0);
out.texcoord = in.a_texcoord;
return out;
};
fragment half4 plain_fragment_vert(out_vertex_t vert [[stage_in]],
texture2d<float, access::sample> colorSampler [[texture(0)]])
{
float4 FragmentColor = colorSampler.sample( s, vert.texcoord);
return half4(FragmentColor);
};
At the end of this, my scene renders, but I'm just not getting the desired effects.
The first question would be...is motion blur possible with a system like this, I don't want to chase this any-longer if its not.
The second is, where am I going wrong?
The complete technique for completion:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>passes</key>
<dict>
<key>drawEnemies</key>
<dict>
<key>draw</key>
<string>DRAW_SCENE</string>
<key>includeCategoryMask</key>
<integer>2</integer>
<key>excludeCategoryMask</key>
<integer>1</integer>
<key>program</key>
<string>doesntexist</string>
<key>metalVertexShader</key>
<string>multi_vertex</string>
<key>metalFragmentShader</key>
<string>multi_fragment_vert</string>
<key>inputs</key>
<dict>
<key>colorSampler</key>
<string>COLOR</string>
<key>a_texcoord</key>
<string>a_texcoord-symbol</string>
<key>aPos</key>
<string>vertexSymbol</string>
</dict>
<key>outputs</key>
<dict>
<key>color</key>
<string>enemiesColor</string>
</dict>
</dict>
<key>blurEnemies</key>
<dict>
<key>draw</key>
<string>DRAW_QUAD</string>
<key>program</key>
<string>doesntexist</string>
<key>metalVertexShader</key>
<string>blur_vertex</string>
<key>metalFragmentShader</key>
<string>blur_fragment_vert</string>
<key>inputs</key>
<dict>
<key>colorSampler</key>
<string>COLOR</string>
<key>enemyColor</key>
<string>enemiesColor</string>
<key>a_texcoord</key>
<string>a_texcoord-symbol</string>
</dict>
<key>outputs</key>
<dict>
<key>color</key>
<string>chrisColor</string>
</dict>
</dict>
<key>blendTogether</key>
<dict>
<key>draw</key>
<string>DRAW_SCENE</string>
<key>program</key>
<string>doesntexist</string>
<key>metalVertexShader</key>
<string>plain_vertex</string>
<key>metalFragmentShader</key>
<string>plain_fragment_vert</string>
<key>inputs</key>
<dict>
<key>colorSampler</key>
<string>COLOR</string>
<key>aPos</key>
<string>vertexSymbol</string>
<key>a_texcoord</key>
<string>a_texcoord-symbol</string>
</dict>
<key>outputs</key>
<dict>
<key>color</key>
<string>COLOR</string>
</dict>
</dict>
</dict>
<key>sequence</key>
<array>
<string>blendTogether</string>
</array>
<key>targets</key>
<dict>
<key>enemiesColor</key>
<dict>
<key>type</key>
<string>color</string>
</dict>
<key>chrisColor</key>
<dict>
<key>type</key>
<string>color</string>
</dict>
</dict>
<key>symbols</key>
<dict>
<key>a_texcoord-symbol</key>
<dict>
<key>semantic</key>
<string>texcoord</string>
</dict>
<key>vertexSymbol</key>
<dict>
<key>semantic</key>
<string>vertex</string>
</dict>
</dict>
</dict>
</plist>
If you can wait until macOS Sierra/iOS 10, the new SceneKit HDI camera supports motion blur.
Video: https://developer.apple.com/videos/play/wwdc2016/609/
Source; https://developer.apple.com/library/prerelease/content/samplecode/Badger/Introduction/Intro.html