Basic DOF Shader

Basic DOF shader with multiple levels of blur. Adapted from bloom posteffect.

Settings are:

focus - the depth to focus at
focalRange - the number of meters to go from focus to as unfocused as the effect can get
focalZone - the number of meters at the focus point that are in focus.
camera - must be set to the camera the effect is attached to
samples - the number of samples to take (affects performance, I’d use between 6 and 12)

var SAMPLE_GROUPS = 14;

function computeGaussian(n, theta) {
    return ((1.0 / Math.sqrt(2 * Math.PI * theta)) * Math.exp(-(n * n) / (2 * theta * theta)));
}

function calculateBlurValues(sampleWeights, sampleOffsets, dx, dy, sampleCount) {
    var i, len;
    for (let j = 0; j < SAMPLE_GROUPS; j++) {
        var base = j * sampleCount
        var totalWeights = 0;
        var baseOffsets = j * sampleCount * 2
        for (i = 0, len = Math.floor(sampleCount / 2); i < len; i++) {
            // Store weights for the positive and negative taps.
            var weight = computeGaussian(i + 1, j + 1);
            sampleWeights[i * 2 + base] = weight;
            sampleWeights[i * 2 + 1 + base] = weight;
            totalWeights += weight;

            var sampleOffset = i + 0.5;

            // Store texture coordinate offsets for the positive and negative taps.
            sampleOffsets[i * 4 + baseOffsets] = dx * sampleOffset;
            sampleOffsets[i * 4 + 1 + baseOffsets] = dy * sampleOffset;
            sampleOffsets[i * 4 + 2 + baseOffsets] = -dx * sampleOffset;
            sampleOffsets[i * 4 + 3 + baseOffsets] = -dy * sampleOffset;
        }
        // Normalize the list of sample weightings, so they will always sum to one.
        for (i = 0, len = sampleCount; i < len; i++) {
            sampleWeights[i + base] /= totalWeights * 4;
        }
    }
}


var DOFEffect = pc.inherits(function (graphicsDevice) {
    var device = this.device = graphicsDevice
    this.samples = 10
    this.focus = 10
    this.focalRange = 8
    this.focalZone = 2
    this.blurShader = pc.shaderChunks.createShaderFromCode(device, pc.shaderChunks.fullscreenQuadVS, blur, "blur")
}, pc.PostEffect)

DOFEffect.prototype.configure = function() {
    var device = this.device
    this.samples = pc.math.clamp(this.samples, 2, 20)
    if(this._lastWidth === device.width && this._lastHeight === device.height && this.samples === this._lastSamples) return
    this._lastWidth = device.width, this._lastHeight = device.height
    this.sampleWeightsWidth = new Float32Array(SAMPLE_COUNT * SAMPLE_GROUPS)
    this.sampleOffsetsWidth = new Float32Array(SAMPLE_COUNT * 2 * SAMPLE_GROUPS)
    this.sampleWeightsHeight = new Float32Array(SAMPLE_COUNT * SAMPLE_GROUPS)
    this.sampleOffsetsHeight = new Float32Array(SAMPLE_COUNT * 2 * SAMPLE_GROUPS)
    calculateBlurValues(this.sampleWeightsWidth, this.sampleOffsetsWidth, 1.0/device.width, 0, this.samples)
    calculateBlurValues(this.sampleWeightsHeight, this.sampleOffsetsHeight, 0, 1.0/device.height, this.samples)
}

DOFEffect.prototype.render = function (input, output) {
    var device = this.device
    var scope = device.scope
    var camera = this.camera
    if(!camera) return
    this.configure()

    var focalDepth = (this.focus - camera.nearClip) / (camera.farClip - camera.nearClip)
    var focalRange = (this.focalRange) / (camera.farClip - camera.nearClip)
    var focalZone = (this.focalZone) / (camera.farClip - camera.nearClip)

    scope.resolve("uBlurWeights[0]").setValue(this.sampleWeightsWidth)
    scope.resolve("uBlurOffsets[0]").setValue(this.sampleOffsetsWidth)
    scope.resolve("uBlurTexture").setValue(input.colorBuffer)
    scope.resolve("uFocus").setValue(focalDepth)
    scope.resolve("uFocalRange").setValue(focalRange)
    scope.resolve("uFocalZone").setValue(focalZone)
    scope.resolve("samples").setValue(this.samples)
    pc.drawQuadWithShader(device, output, this.blurShader)
    scope.resolve("uBlurWeights[0]").setValue(this.sampleWeightsHeight)
    scope.resolve("uBlurOffsets[0]").setValue(this.sampleOffsetsHeight)
    device.setBlending(true)
    device.setBlendFunction(pc.BLENDMODE_ONE, pc.BLENDMODE_ONE)
    pc.drawQuadWithShader(device, output, this.blurShader,null, null, true)
    device.setBlending(false)
}

Without DOF:

With:

Requires a blur variable set to the shader:

#define MAX_SAMPLE_COUNT 24
#define MAX_SAMPLE_GROUPS 14

varying vec2 vUv0;
uniform sampler2D uDepthMap;
uniform sampler2D uBlurTexture;
uniform vec2 uBlurOffsets[MAX_SAMPLE_COUNT*MAX_SAMPLE_GROUPS];
uniform float uBlurWeights[MAX_SAMPLE_COUNT*MAX_SAMPLE_GROUPS];
uniform float uFocus;
uniform float uFocalRange;
uniform float uFocalZone;
uniform int samples;

float unpackFloat(vec4 rgbaDepth) {
    const vec4 bitShift = vec4(1.0 / (256.0 * 256.0 * 256.0), 1.0 / (256.0 * 256.0), 1.0 / 256.0, 1.0);
    float depth = dot(rgbaDepth, bitShift);
    return depth;
}

//Blurs in a single direction
void main(void)
{
    int map;
    vec4 color = vec4(0.0);
    float depth = unpackFloat(texture2D(uDepthMap, vUv0));
    float focalDepth =depth - uFocus;
    float amount = clamp((abs(focalDepth)-uFocalZone) / uFocalRange,0.0,1.0);

    if(amount > 0.0) {
        map = int(amount * 13.0) * samples;
        int length = map + samples;
        for (int i = map; i < length; i++) {
            color += texture2D(uBlurTexture, vUv0 + uBlurOffsets[i]) * uBlurWeights[i];
        }
    } else {
        color = texture2D(uBlurTexture, vUv0) * 0.5;
    }
    gl_FragColor = color;
}

   
5 Likes

If you need Bokeh - I’m nearly there with a much more “blurry” DOF. As for the script - I will. I use legacy scripting so it takes me a while to convert it to the other style.

Bokeh Shader project

https://playcanvas.com/project/501614/overview/bokeh

2 Likes

No problem - just be aware - it’s very expensive when it’s very blurry! I’m going to look at another way of splitting out the higher blur levels. If you can live with lower powers of blur then it should be fine. At the highest levels it’s doing a lot of texture samples. But I thought it might be useful for something. It’s a lot more blurry than my Gaussian filter - a lower power one still gives a good effect.

Looks like you are missing the shaders… there are 3


I need your help my friend .It have error when I launch with your script .Do you know why ?

2 Likes

hi @yuheng_chen and welcome,

That’s an old thread! If you are looking for a depth of field post process effect there is one in the engine repo:

1 Like

I got the same problem when I start up the project with the latest engine. It won’t report errors when I use a previous stable engine, but it seems still not work very well. There are some differences between:

Mike’s Version (just play without open editor)

Launch with engine version 1.15.3 Click to play

Are some properties set incorrectly? Or what?

@whydoidoit

Just an update for the link.
Here are all posteffects now:
engine/scripts/posteffects at master · playcanvas/engine (github.com)

and here’s an example
http://playcanvas.github.io/#/graphics/post-effects

I’m playing around with the Bokeh script that’s posted here:

But I get a weird issue, the effect is only visible behind the scene assets, creating a “halo” around them. What am I doing wrong?
https://playcanvas.com/project/831825/overview/bokeh-test

Hi @MrOliv,

I think everything is correctly set in your project, it seems the effect has a problem when rendering over a viewport portion where there is no model on the background. Seems depth related? @mvaligursky and idea?

I’ve added a temp skybox model using a sphere and your env_01 texture and that solve the problem:

The bots have depth test and depth write turned off on their materials … is that on purpose? If they don’t write to depth, there is no depth buffer for the DOF to work on.
(not sure that is the only problem).

1 Like