Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write a sceneKit shader modifier for a dissolve in effect

I'd like to build a dissolve in effect for a Scenekit game. I've been looking into shader modifiers since they seem to be the most light weight and haven't had any luck in replicating this effect:

Dissolve shader effect

Is it possible to use shader modifiers to create this effect? How would you go about implementing one?

like image 711
soCohesive Avatar asked Feb 06 '19 20:02

soCohesive


1 Answers

You can get pretty close to the intended effect with a fragment shader modifier. The basic approach is as follows:

  • Sample from a noise texture
  • If the noise sample is below a certain threshold (which I call "revealage"), discard it, making it fully transparent
  • Otherwise, if the fragment is close to the edge, replace its color with your preferred edge color (or gradient)
  • Apply bloom to make the edges glow

Here's the shader modifier code for doing this:

#pragma arguments

float revealage;
texture2d<float, access::sample> noiseTexture;

#pragma transparent
#pragma body

const float edgeWidth = 0.02;
const float edgeBrightness = 2;
const float3 innerColor = float3(0.4, 0.8, 1);
const float3 outerColor = float3(0, 0.5, 1);
const float noiseScale = 3;

constexpr sampler noiseSampler(filter::linear, address::repeat);
float2 noiseCoords = noiseScale * _surface.ambientTexcoord;
float noiseValue = noiseTexture.sample(noiseSampler, noiseCoords).r;

if (noiseValue > revealage) {
    discard_fragment();
}

float edgeDist = revealage - noiseValue;
if (edgeDist < edgeWidth) {
    float t = edgeDist / edgeWidth;
    float3 edgeColor = edgeBrightness * mix(outerColor, innerColor, t);
    _output.color.rgb = edgeColor;
}

Notice that the revealage parameter is exposed as a material parameter, since you might want to animate it. There are other internal constants, such as edge width and noise scale that can be fine-tuned to get the desired effect with your content.

Different noise textures produce different dissolve effects, so you can experiment with that as well. I just used this multioctave value noise image:

Value noise image used as dissolve texture

Load the image as a UIImage or NSImage and set it on the material property that gets exposed as noiseTexture:

material.setValue(SCNMaterialProperty(contents: noiseImage), forKey: "noiseTexture")

You'll need to add bloom as a post-process to get that glowy, e-wire effect. In SceneKit, this is as simple as enabling the HDR pipeline and setting some parameters:

let camera = SCNCamera()
camera.wantsHDR = true
camera.bloomThreshold = 0.8
camera.bloomIntensity = 2
camera.bloomBlurRadius = 16.0
camera.wantsExposureAdaptation = false

All of the numeric parameters will potentially need to be tuned to your content.

To keep things tidy, I prefer to keep shader modifiers in their own text files (I named mine "dissolve.fragment.txt"). Here's how to load some modifier code and attach it to a material.

let modifierURL = Bundle.main.url(forResource: "dissolve.fragment", withExtension: "txt")!
let modifierString = try! String(contentsOf: modifierURL)
material.shaderModifiers = [
    SCNShaderModifierEntryPoint.fragment : modifierString
]

And finally, to animate the effect, you can use a CABasicAnimation wrapped with a SCNAnimation:

let revealAnimation = CABasicAnimation(keyPath: "revealage")
revealAnimation.timingFunction = CAMediaTimingFunction(name: .linear)
revealAnimation.duration = 2.5
revealAnimation.fromValue = 0.0
revealAnimation.toValue = 1.0
let scnRevealAnimation = SCNAnimation(caAnimation: revealAnimation)
material.addAnimation(scnRevealAnimation, forKey: "Reveal")

Et voila!

like image 100
warrenm Avatar answered Oct 23 '22 05:10

warrenm