common.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);
}
edge.frag
#version 300 es
precision highp float;
const float R_LUMINANCE = 0.298912;
const float G_LUMINANCE = 0.586611;
const float B_LUMINANCE = 0.114478;
float toMonochrome(vec3 color) {
return dot(color, vec3(R_LUMINANCE, G_LUMINANCE, B_LUMINANCE));
}
vec3 rgb2hsv(vec3 c) {
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
vec3 hsv2rgb(vec3 color) {
float hue = color.x * 6.0;
vec3 m = mod(hue + vec3(0.0, 4.0, 2.0), 6.0);
vec3 a = abs(m - 3.0);
vec3 rgb = clamp(a - 1.0, 0.0, 1.0);
return color.z * mix(vec3(1.0), rgb, color.y);
}
float posterizeHue(float hue, int level) {
float hueStep = 255.0 / float(level - 1);
float newHue = hue * 360.0;
newHue = floor(newHue / hueStep + 0.5) * hueStep;
newHue /= 360.0;
return newHue;
}
float posterizeColorRatio(float ratio, int level) {
float ratioStep = 255.0 / float(level - 1);
float unclamp = ratio * 255.0;
float newRatio = floor(unclamp / ratioStep + 0.5) * ratioStep;
newRatio /= 255.0;
return newRatio;
}
vec2[9] offset3x3(vec2 texelSize) {
vec2 offset[9];
offset[0] = vec2(-texelSize.x, -texelSize.y);
offset[1] = vec2( 0.0, -texelSize.y);
offset[2] = vec2( texelSize.x, -texelSize.y);
offset[3] = vec2(-texelSize.x, 0.0);
offset[4] = vec2( 0.0, 0.0);
offset[5] = vec2( texelSize.x, 0.0);
offset[6] = vec2(-texelSize.x, texelSize.y);
offset[7] = vec2( 0.0, texelSize.y);
offset[8] = vec2( texelSize.x, 1.0);
return offset;
}
vec3 offsetLookup(sampler2D tex, vec2 center, vec2 offset) {
return texture(tex, center + offset).rgb;
}
vec3 applyKernelXY(sampler2D tex, vec2 texelSize, vec2 center, float[9] kernelX, float[9] kernelY) {
vec2[9] offset = offset3x3(texelSize);
float dx = 0.0;
float dy = 0.0;
vec3 color;
float el;
for (int i = 0; i < 9; i++) {
color = offsetLookup(tex, center, offset[i]);
el = rgb2hsv(color).b;
dx += el * kernelX[i];
dy += el * kernelY[i];
}
float result = length(vec2(dx, dy));
return vec3(result);
}
uniform sampler2D uTexture0;
uniform float uGamma;
uniform float uHue;
uniform float uSaturation;
uniform float uBrightness;
uniform int uLevelH;
uniform int uLevelS;
uniform int uLevelB;
uniform vec3 uMinDensity;
in vec2 vTextureCoords;
layout (location = 0) out vec4 fragColor1;
layout (location = 1) out vec4 fragColor2;
void main() {
ivec2 textureSize = textureSize(uTexture0, 0);
vec2 texelSize = 1.0 / vec2(float(textureSize.x), float(textureSize.y));
vec2 texCoord = vec2(vTextureCoords.x, 1.0 - vTextureCoords.y);
vec3 inputColor = texture(uTexture0, texCoord).rgb;
vec3 outColor1 = pow(inputColor, vec3(uGamma));
vec3 hsv = rgb2hsv(outColor1);
hsv.r = uLevelH == 1 ? uHue / 360.0 : posterizeHue(hsv.r, uLevelH);
hsv.g = uLevelS == 1 ? uSaturation : posterizeColorRatio(hsv.g, uLevelS);
hsv.b = uLevelB == 1 ? uBrightness : posterizeColorRatio(hsv.b, uLevelB);
outColor1 = hsv2rgb(hsv);
outColor1 = mix(outColor1, vec3(1.0), uMinDensity);
float[9] kernelX = float[](
-1.0, 0.0, 1.0,
-1.0, 0.0, 1.0,
-1.0, 0.0, 1.0
);
float[9] kernelY = float[](
-1.0, -1.0, -1.0,
0.0, 0.0, 0.0,
1.0, 1.0, 1.0
);
vec3 outColor2 = applyKernelXY(uTexture0, texelSize, texCoord, kernelX, kernelY);
outColor2 = vec3(toMonochrome(outColor2));
fragColor1 = vec4(outColor1, 1.0);
fragColor2 = vec4(outColor2, 1.0);
}
common.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);
}
draw-stroke.frag
#version 300 es
precision highp float;
const float PI = 3.1415926;
const float R_LUMINANCE = 0.298912;
const float G_LUMINANCE = 0.586611;
const float B_LUMINANCE = 0.114478;
float toMonochrome(vec3 color) {
return dot(color, vec3(R_LUMINANCE, G_LUMINANCE, B_LUMINANCE));
}
const uint UINT_MAX = 0xffffffffu;
uvec3 k = uvec3(0x456789abu, 0x6789ab45u, 0x89ab4567u);
uvec3 u = uvec3(1, 2, 3);
uvec2 uhash22(uvec2 n){
n ^= (n.yx << u.xy);
n ^= (n.yx >> u.xy);
n *= k.xy;
n ^= (n.yx << u.xy);
return n * k.xy;
}
vec2 hash22(vec2 b) {
uvec2 n = floatBitsToUint(b);
return vec2(uhash22(n)) / vec2(UINT_MAX);
}
float hash21(vec2 b) {
uvec2 n = floatBitsToUint(b);
return float(uhash22(n).x) / float(UINT_MAX);
}
float hash21Clamp(vec2 b, float minV, float maxV) {
return ((maxV - minV) * hash21(b)) + minV;
}
vec3 lighten(vec3 b, vec3 f) {
return max(b, f);
}
vec3 screen(vec3 b, vec3 f) {
return 1.0 - (1.0 - b) * (1.0 - f);
}
vec3 overlay(vec3 b, vec3 f) {
float bmpness = max(b.r, max(b.g, b.b));
return mix(
2.0 * b * f,
1.0 - 2.0 * (1.0 - b) * (1.0 - f),
step(0.5, bmpness)
);
}
vec3 colorburn(vec3 b, vec3 f) {
return 1.0 - (1.0 - b) / f;
}
vec3 colordodge(vec3 b, vec3 f) {
return b / (1.0 - f);
}
uniform sampler2D uTexture1;
uniform sampler2D uTexture3;
in vec2 vTextureCoords;
out vec4 fragColor;
void main() {
vec2 texCoord = vec2(vTextureCoords.x, 1.0 - vTextureCoords.y);
vec3 posterized = texture(uTexture3, texCoord).rgb;
vec3 edge = texture(uTexture1, texCoord).rgb;
float grayEdge = toMonochrome(edge);
float dx = dFdx(grayEdge);
float dy = dFdy(grayEdge);
float magnitude = length(vec2(dx, dy));
float c = hash21(texCoord);
vec3 pp = texture(uTexture3, c + vec2(dx, dy) * magnitude).rgb;
vec3 mp = texture(uTexture3, c + vec2(-dx, dy) * magnitude).rgb;
vec3 pm = texture(uTexture3, c + vec2(dx, -dy) * magnitude).rgb;
vec3 mm = texture(uTexture3, c + vec2(-dx, -dy) * magnitude).rgb;
vec3 hatchR = posterized + mm;
hatchR -= pp;
vec3 hatchL = posterized + mp;
hatchL -= pm;
vec3 sketch = hatchR * hatchL;
sketch = vec3(toMonochrome(sketch));
vec3 outColor = vec3(1.0) - sketch * edge;
fragColor = vec4(outColor, 1.0);
}
render.ts
import { SketchFilter, FilterSketchConfig, FilterSketchFn } from "sketchgl"
import { Uniforms, Program } from "sketchgl/program"
import type { RawVector3 } from "sketchgl/math"
import { CanvasCoverPolygon } from "sketchgl/geometry"
import { ImageTexture } from "sketchgl/texture"
import { MRTRenderer } from "sketchgl/renderer"
import vertSrc from "./common.vert?raw"
import fragSrcForEdge from "./edge.frag?raw"
import fragSrcForDrawStroke from "./draw-stroke.frag?raw"
import imageCubeLogo from "@/assets/original/cat.jpg"
import imageGoldfishBowl from "@/assets/original/japanese-style_00011.jpg"
import imageAutumnLeaves from "@/assets/original/autumn-leaves_00037.jpg"
import imageTree from "@/assets/original/tree-woods_00123.jpg"
const sketch: FilterSketchFn = ({ gl, canvas, fitImage }) => {
const uniformsFor = {
level: new Uniforms(gl, [
"uTexture0",
"uGamma",
"uHue",
"uSaturation",
"uBrightness",
"uLevelH",
"uLevelS",
"uLevelB",
"uMinDensity"
]),
drawStroke: new Uniforms(gl, ["uTexture1", "uTexture3"])
}
let uGamma = 0.5
let uHue = 0.0
let uSaturation = 1.0
let uBrightness = 1.0
let uLevelH = 256
let uLevelS = 2
let uLevelB = 128
let uMinDensity: RawVector3 = [0.3, 0.3, 0.3]
const images = [
{ name: "木", src: imageTree },
{ name: "猫", src: imageCubeLogo },
{ name: "金魚鉢", src: imageGoldfishBowl },
{ name: "紅葉", src: imageAutumnLeaves }
]
const imageNames = images.map((obj) => obj.name)
const textures = images.map((img) => new ImageTexture(gl, img.src))
let activeImage = 1
const renderer = new MRTRenderer(gl, canvas, vertSrc, fragSrcForEdge, { texCount: 2 })
uniformsFor.level.init(renderer.glProgramForOffscreen)
const program = new Program(gl)
program.attach(vertSrc, fragSrcForDrawStroke)
uniformsFor.drawStroke.init(program.glProgram)
const plane = new CanvasCoverPolygon(gl)
plane.setLocations({ vertices: 0, uv: 1 })
gl.clearColor(1.0, 1.0, 1.0, 1.0)
gl.clearDepth(1.0)
return {
resize: [renderer.resize],
preload: [...textures.map((tex) => tex.load())],
preloaded: [() => fitImage(textures[activeImage].img)],
drawOnFrame() {
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height)
plane.bind()
renderer.switchToOffcanvas()
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
uniformsFor.level.float("uGamma", uGamma)
uniformsFor.level.float("uHue", uHue)
uniformsFor.level.float("uSaturation", uSaturation)
uniformsFor.level.float("uBrightness", uBrightness)
uniformsFor.level.int("uLevelH", uLevelH)
uniformsFor.level.int("uLevelS", uLevelS)
uniformsFor.level.int("uLevelB", uLevelB)
uniformsFor.level.fvector3("uMinDensity", uMinDensity)
textures[activeImage].activate(renderer.glProgramForOffscreen!, "uTexture0")
plane.draw({ primitive: "TRIANGLES" })
renderer.switchToCanvas(program)
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
renderer.useAsTexture(0, "uTexture3", program.glProgram)
renderer.useAsTexture(1, "uTexture1", program.glProgram)
plane.draw({ primitive: "TRIANGLES" })
},
control(ui) {
ui.select("Image", imageNames[activeImage], imageNames, (name) => {
const idx = imageNames.indexOf(name)
if (idx < 0) return
activeImage = idx
fitImage(textures[activeImage].img)
})
ui.number("Gamma", uGamma, 0.0, 1.0, 0.01, (value) => {
uGamma = value
})
ui.number("HSV.h", uHue, 0, 360, 1, (value) => {
uHue = value
})
ui.number("HSV.s", uSaturation, 0.0, 1.0, 0.01, (value) => {
uSaturation = value
})
ui.number("Level HSV.h", uLevelH, 1, 256, 1, (value) => {
uLevelH = value
})
ui.number("Level HSV.s", uLevelS, 1, 256, 1, (value) => {
uLevelS = value
})
ui.rgb("Min Density", uMinDensity, (color) => {
uMinDensity = color
})
}
}
}
export const onload = () => {
const config: FilterSketchConfig = {
canvas: {
el: "gl-canvas",
autoResize: true
}
}
SketchFilter.init(config, sketch)
}