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;

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

// 符号なし整数の最大値
const uint UINT_MAX = 0xffffffffu;

// 算術積に使う大きな桁数の定数
uint k1 = 0x456789abu;

// 符号なし整数の1dハッシュ関数
uint uhash11(uint n) {
  n ^= (n << 1);
  n ^= (n >> 1);
  n *= k1;
  n ^= (n << 1);
  return n * k1;
}

// 浮動小数点数の1dハッシュ関数
float hash11(float b) {
  // ビット列を符号なし整数に変換
  uint n = floatBitsToUint(b);
  // 値の正規化
  return float(uhash11(n)) / float(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;
  
  vec2 pos = vec2(0.0);

  // ある方向に擦った感じにしたい場合、hash11を使う
  pos.x = clamp_range(hash11(uv.x), 0.0, texelSize.x);
  pos.y = clamp_range(hash11(uv.y), 0.0, texelSize.y);
  
  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 = 1.0
  let uSiteCount = 12

  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, 30, 1, (v) => {
        uSiteCount = v
      })
    }
  }
}

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