読者です 読者をやめる 読者になる 読者になる

S.F. Page

Programming,Music,etc...

CRT風ポストエフェクトを追加してみる

Shader Toyのコードを拝借して若干手を加え、CRT風ポストエフェクトを追加してみた。比較のために画面の右半分だけにエフェクトを追加している。

元のポストエフェクトのコードは以下。

https://www.shadertoy.com/view/4scSR8

なかなかレトロな雰囲気を醸し出してよい。元コードはCRTっぽく湾曲して表示するエフェクトあった(warp())が、それは適用していない。

動くサンプルはこちら:

http://github.sfpgmr.net/2dshooting2/devver/20170416/

レポジトリ:

https://github.com/sfpgmr/2dshooting2

ポストエフェクトはthree.jsのexampleにあるEffectComposerというのを使っている。 Passを継承したクラスにシェーダーコードを書いて、それをEffectComposerに追加していくと、エフェクトがチェインされ、実行される。いくつかのPassもexampleに入っているので、それを参考にすれば比較的簡単にポストエフェクトを書くことができる。

ちなみに今回作成したPassは下記。シェーダーコードはShaderToyのものをほぼそのまま使わせてもらっている。 ただShaderToyのみで使える変数があったりするので、three.js上で動くように修正する必要はある。

/**
 * @author SFPGMR
 */
// Shader Toyより拝借して少し改造
// https://www.shadertoy.com/view/4scSR8
// by Timothy Lottes
//
"use strict";

let vertexShader =
  `
varying vec2 vUv;
void main() {
        vUv = uv;
    gl_Position = vec4( position, 1.0 );
  }
`;
let fragmentShader =
  `
uniform sampler2D tDiffuse;
uniform vec2 resolution;
uniform float time;
varying vec2 vUv;

#define RGBA(r, g, b, a)    vec4(float(r)/255.0, float(g)/255.0, float(b)/255.0, float(a)/255.0)

//const vec3 kBackgroundColor = RGBA(0x00, 0x00, 0x00, 0x00).rgb; // medium-blue sky
const vec3 kBackgroundColor = RGBA(0xff, 0x00, 0xff, 0xff).rgb; // test magenta

// Emulated input resolution.
// Fix resolution to set amount.
// Note: 256x224 is the most common resolution of the SNES, and that of Super Mario World.
vec2 res = vec2(
  640.0 / 1.0,
  480.0 / 1.0
);

// Hardness of scanline.
// -8.0 = soft
// -16.0 = medium
float sHardScan = -8.0;

// Hardness of pixels in scanline.
// -2.0 = soft
// -4.0 = hard
const float kHardPix = -3.0;

// Display warp.
// 0.0 = none
// 1.0 / 8.0 = extreme
const vec2 kWarp = vec2(1.0 / 32.0, 1.0 / 24.0);
//const vec2 kWarp = vec2(0);

// Amount of shadow mask.
float kMaskDark = 0.5;
float kMaskLight = 1.5;

//------------------------------------------------------------------------

// sRGB to Linear.
// Assuing using sRGB typed textures this should not be needed.
float toLinear1(float c) {
    return (c <= 0.04045) ?
        (c / 12.92) :
        pow((c + 0.055) / 1.055, 2.4);
}
vec3 toLinear(vec3 c) {
    return vec3(toLinear1(c.r), toLinear1(c.g), toLinear1(c.b));
}

// Linear to sRGB.
// Assuing using sRGB typed textures this should not be needed.
float toSrgb1(float c) {
    return(c < 0.0031308 ?
        (c * 12.92) :
        (1.055 * pow(c, 0.41666) - 0.055));
}
vec3 toSrgb(vec3 c) {
    return vec3(toSrgb1(c.r), toSrgb1(c.g), toSrgb1(c.b));
}

// Nearest emulated sample given floating point position and texel offset.
// Also zero's off screen.
vec4 fetch(vec2 pos, vec2 off)
{
    pos = floor(pos * res + off) / res;
    if (max(abs(pos.x - 0.5), abs(pos.y - 0.5)) > 0.5)
        return vec4(vec3(0.0), 0.0);
    
//    vec4 sampledColor = texture(iChannel0, pos.xy, -16.0);
    vec4 sampledColor = texture2D(tDiffuse, pos.xy, -16.0);
    
    sampledColor = vec4(
        (sampledColor.rgb * sampledColor.a) +
            (kBackgroundColor * (1.0 - sampledColor.a)),
        1.0
    );
    
    return vec4(
        toLinear(sampledColor.rgb),
        sampledColor.a
    );
}

// Distance in emulated pixels to nearest texel.
vec2 dist(vec2 pos) {
    pos = pos * res;
    return -((pos - floor(pos)) - vec2(0.5));
}

// 1D Gaussian.
float gaus(float pos, float scale) {
    return exp2(scale * pos * pos);
}

// 3-tap Gaussian filter along horz line.
vec3 horz3(vec2 pos, float off)
{
    vec3 b = fetch(pos, vec2(-1.0, off)).rgb;
    vec3 c = fetch(pos, vec2( 0.0, off)).rgb;
    vec3 d = fetch(pos, vec2(+1.0, off)).rgb;
    float dst = dist(pos).x;
    // Convert distance to weight.
    float scale = kHardPix;
    float wb = gaus(dst - 1.0, scale);
    float wc = gaus(dst + 0.0, scale);
    float wd = gaus(dst + 1.0, scale);
    // Return filtered sample.
    return (b * wb + c * wc + d * wd) / (wb + wc + wd);
}

// 5-tap Gaussian filter along horz line.
vec3 horz5(vec2 pos, float off)
{
    vec3 a = fetch(pos, vec2(-2.0, off)).rgb;
    vec3 b = fetch(pos, vec2(-1.0, off)).rgb;
    vec3 c = fetch(pos, vec2( 0.0, off)).rgb;
    vec3 d = fetch(pos, vec2(+1.0, off)).rgb;
    vec3 e = fetch(pos, vec2(+2.0, off)).rgb;
    float dst = dist(pos).x;
    // Convert distance to weight.
    float scale = kHardPix;
    float wa = gaus(dst - 2.0, scale);
    float wb = gaus(dst - 1.0, scale);
    float wc = gaus(dst + 0.0, scale);
    float wd = gaus(dst + 1.0, scale);
    float we = gaus(dst + 2.0, scale);
    // Return filtered sample.
    return (a * wa + b * wb + c * wc + d * wd + e * we) / (wa + wb + wc + wd + we);
}

// Return scanline weight.
float scan(vec2 pos, float off) {
    float dst = dist(pos).y;
    return gaus(dst + off, sHardScan);
}

// Allow nearest three lines to effect pixel.
vec3 tri(vec2 pos)
{
    vec3 a = horz3(pos, -1.0);
    vec3 b = horz5(pos,  0.0);
    vec3 c = horz3(pos, +1.0);
    float wa = scan(pos, -1.0);
    float wb = scan(pos,  0.0);
    float wc = scan(pos, +1.0);
    return a * wa + b * wb + c * wc;
}

// Distortion of scanlines, and end of screen alpha.
vec2 warp(vec2 pos)
{
    pos = pos * 2.0 - 1.0;
    pos *= vec2(
        1.0 + (pos.y * pos.y) * kWarp.x,
        1.0 + (pos.x * pos.x) * kWarp.y
    );
    return pos * 0.5 + 0.5;
}

// Shadow mask.
vec3 mask(vec2 pos)
{
    pos.x += pos.y * 3.0;
    vec3 mask = vec3(kMaskDark, kMaskDark, kMaskDark);
    pos.x = fract(pos.x / 6.0);
    if (pos.x < 0.333)
        mask.r = kMaskLight;
    else if (pos.x < 0.666)
        mask.g = kMaskLight;
    else
        mask.b = kMaskLight;
    return mask;
}

// Draw dividing bars.
float bar(float pos, float bar) {
    pos -= bar;
    return (pos * pos < 4.0) ? 0.0 : 1.0;
}

float rand(vec2 co) {
    return fract(sin(dot(co.xy , vec2(12.9898, 78.233))) * 43758.5453);
}

// Entry.
void main()
{
//    vec2 pos = warp(gl_FragCoord.xy / resolution.xy);
    vec2 pos = gl_FragCoord.xy / resolution.xy;
    
      // Unmodified.
    if(gl_FragCoord.x > resolution.x * 0.5){
        vec3 c = tri(pos) * mask(gl_FragCoord.xy);
    gl_FragColor = vec4(
        toSrgb(c),
        1.0
    );
    } else {
      gl_FragColor = texture2D(tDiffuse,vUv);
    }
}
`;

//     let geometry = new THREE.PlaneBufferGeometry( 1920, 1080 );
let uniforms = {
  tDiffuse: { value: null },
  resolution: { value: new THREE.Vector2() },
  time: { value: 0.0 }
};
//     uniforms.resolution.value.x = WIDTH;
//     uniforms.resolution.value.y = HEIGHT;
//     let material = new THREE.ShaderMaterial( {
//       uniforms: uniforms,
//       vertexShader: vertShader,
//       fragmentShader: fragShader
//     } );
//     let mesh = new THREE.Mesh( geometry, material );
//     mesh.position.z = -5000;
//     scene.add( mesh );
//   }

export default class SFCrtShaderPass extends THREE.Pass {
  constructor(width, height) {
    super();

    this.uniforms = THREE.UniformsUtils.clone(uniforms);
    this.uniforms.resolution.value.x = width;
    this.uniforms.resolution.value.y = height;
    this.material = new THREE.ShaderMaterial({
      uniforms: this.uniforms,
      vertexShader: vertexShader,
      fragmentShader: fragmentShader
    });

    this.camera = new THREE.OrthographicCamera(- 1, 1, 1, - 1, 0, 1);
    this.scene = new THREE.Scene();

    this.quad = new THREE.Mesh(new THREE.PlaneBufferGeometry(2, 2), null);
    this.scene.add(this.quad);

  }

  setSize(width, height) {
    this.uniforms.resolution.value.x = width;
    this.uniforms.resolution.value.y = height;
  }

  render(renderer, writeBuffer, readBuffer, delta, maskActive) {
    this.uniforms["tDiffuse"].value = readBuffer.texture;
    this.quad.material = this.material;

    if (this.renderToScreen) {

      renderer.render(this.scene, this.camera);

    } else {

      renderer.render(this.scene, this.camera, writeBuffer, this.clear);

    }

  }
}