index.vert

#version 300 es

layout (location = 0) in vec3 aVertexPosition;
layout (location = 1) in vec2 aVertexTextureCoords;

out vec2 vTextureCoords;

void main() {
  vTextureCoords = aVertexTextureCoords;
  gl_Position = vec4(aVertexPosition, 1.0);
}

index.frag

#version 300 es

precision highp float;

vec2 clamp_range(vec2 v, vec2 minV, vec2 maxV) {
  return v * (maxV - minV) + minV;
}

// 符号なし整数の最大値
const uint UINT_MAX = 0xffffffffu;
// 算術積に使う大きな桁数の定数
uvec3 k2 = uvec3(0x456789abu, 0x6789ab45u, 0x89ab4567u);
// シフト数
uvec3 u = uvec3(1, 2, 3);

// 符号なし整数の2d => 2dハッシュ関数
uvec2 uhash22(uvec2 n) {
  n ^= n.yx << u.xy;
  n ^= n.yx >> u.xy;
  n *= k2.xy;
  n ^= n.yx << u.xy;
  return n * k2.xy;
}

// 浮動小数点数の2d => 2dハッシュ関数
vec2 hash22(vec2 b) {
  // ビット列を符号なし整数に変換
  uvec2 n = floatBitsToUint(b);
  // 値の正規化
  return vec2(uhash22(n)) / vec2(UINT_MAX);
}

uniform sampler2D uOriginal;
uniform float uAlpha;
uniform float uMixingRatio;
uniform float uSiteCount;

in vec2 vTextureCoords;

out vec4 fragColor;

void main() {
  ivec2 iTextureSize = textureSize(uOriginal, 0);
  vec2 textureSize = vec2(float(iTextureSize.x), float(iTextureSize.y));
  vec2 texelSize = 1.0 / textureSize;
  
  vec2 uv = vec2(vTextureCoords.x, 1.0 - vTextureCoords.y);
  
  vec3 original = texture(uOriginal, uv).rgb;

  // スプレー画のように満遍なくぼかしたい場合、hash22を使う
  // uSiteCountが小さいとクレヨンのように、大きいとスプレーのように見える
  vec2 pos = clamp_range(hash22(uv), vec2(0.0), texelSize);
  
  pos = fract(pos * uSiteCount);

  vec3 random = texture(uOriginal, uv + pos).rgb;
  
  vec3 outColor = mix(original, random, uMixingRatio);
  
  fragColor = vec4(outColor, uAlpha);
}

render.ts

import { SketchFilter, type FilterSketchConfig, type FilterSketchFn } from "sketchgl"
import { ImageTexture } from "sketchgl/texture"
import { Program, Uniforms } from "sketchgl/program"
import { CanvasCoverPolygon } from "sketchgl/geometry"

import mainVertSrc from "./index.vert?raw"
import mainFragSrc from "./index.frag"

import imageGeometry from "@/assets/original/pastel-tomixy.png"
import imageAutumnLeaves from "@/assets/original/autumn-leaves_00037.jpg"
import imageWater from "@/assets/original/japanese-style_00011.jpg"
import imageGoldFish from "@/assets/original/fantasy-unicorn.jpg"

const sketch: FilterSketchFn = ({ gl, fitImage }) => {
  const uniforms = new Uniforms(gl, ["uAlpha", "uSiteCount", "uMixingRatio"])
  let uAlpha = 1.0
  let uMixingRatio = 0.5
  let uSiteCount = 36

  const images = [
    { name: "ユニコーン", src: imageGoldFish },
    { name: "立方体", src: imageGeometry },
    { name: "紅葉", src: imageAutumnLeaves },
    { name: "金魚", src: imageWater }
  ]
  const imageNames = images.map((obj) => obj.name)
  const textures = images.map((img) => new ImageTexture(gl, img.src))
  let activeImage = 2

  const program = new Program(gl)
  program.attach(mainVertSrc, mainFragSrc)
  program.activate()

  uniforms.init(program.glProgram)

  const plane = new CanvasCoverPolygon(gl)
  plane.setLocations({ vertices: 0, uv: 1 })

  gl.clearColor(1.0, 0.0, 0.0, 1.0)
  gl.clearDepth(1.0)

  return {
    preload: [...textures.map((tex) => tex.load())],
    preloaded: [() => fitImage(textures[activeImage].img)],

    drawOnFrame() {
      gl.viewport(0, 0, gl.canvas.width, gl.canvas.height)
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

      plane.bind()
      textures[activeImage].activate(program.glProgram!, "uOriginal")
      uniforms.float("uAlpha", uAlpha)
      uniforms.float("uMixingRatio", uMixingRatio)
      uniforms.float("uSiteCount", uSiteCount)
      plane.draw({ primitive: "TRIANGLES" })
    },

    control(ui) {
      ui.select("Image", images[activeImage].name, imageNames, (name) => {
        const idx = imageNames.indexOf(name)
        if (idx < 0) return
        activeImage = idx
        fitImage(textures[activeImage].img)
      })
      ui.number("全体の透明度", uAlpha, 0.0, 1.0, 0.01, (v) => {
        uAlpha = v
      })
      ui.number("ノイズの透明度", uMixingRatio, 0.0, 1.0, 0.01, (v) => {
        uMixingRatio = v
      })
      ui.number("広がり", uSiteCount, 3, 100, 1, (v) => {
        uSiteCount = v
      })
    }
  }
}

export const onload = () => {
  const config: FilterSketchConfig = {
    canvas: {
      el: "gl-canvas",
      autoResize: true
    }
  }
  SketchFilter.init(config, sketch)
}