diff --git a/font.js b/font.js index bc00f64..e4428ad 100644 --- a/font.js +++ b/font.js @@ -211,43 +211,46 @@ class FontRenderer { }); console.log(canvasFormat); - this.renderPipeline = device.createRenderPipeline({ - label: `${label} pipeline`, - layout: pipelineLayout, - vertex: { - module: shaderModule, - entryPoint: "vertexMain", - }, - fragment: { - module: shaderModule, - entryPoint: "fragmentMain", - targets: [{ - blend: { - color: { - dstFactor: "one-minus-src-alpha", - srcFactor: "src-alpha", + this.renderPipelines = {} + for (let i = 1; i <= 4; i += 3) { + this.renderPipelines[i] = device.createRenderPipeline({ + label: `${label} pipeline sample count ${i}`, + layout: pipelineLayout, + vertex: { + module: shaderModule, + entryPoint: "vertexMain", + }, + fragment: { + module: shaderModule, + entryPoint: "fragmentMain", + targets: [{ + blend: { + color: { + dstFactor: "one-minus-src-alpha", + srcFactor: "src-alpha", + }, + alpha: { + dstFactor: "one-minus-src-alpha", + srcFactor: "src-alpha", + }, + operation: "add", }, - alpha: { - dstFactor: "one-minus-src-alpha", - srcFactor: "src-alpha", - }, - operation: "add", - }, - format: canvasFormat, - }] - }, - primitive: { - topology: "triangle-list", - }, - depthStencil: { - depthWriteEnabled: true, - depthCompare: "always", - format: "depth24plus", - }, - multisample: { - count: 4, - } - }); + format: canvasFormat, + }] + }, + primitive: { + topology: "triangle-list", + }, + depthStencil: { + depthWriteEnabled: true, + depthCompare: "always", + format: "depth24plus", + }, + multisample: { + count: i, + } + }); + } ////////////////////////////////////////////////////////////////////// // sliders @@ -289,29 +292,36 @@ class FontRenderer { ////////////////////////////////////////////////////////////////////// const configuration = updateSliders(this.sliders); - this.sliderArray[0] = configuration.scale; - this.sliderArray[1] = configuration.translateX; - this.sliderArray[2] = configuration.translateY; + + this.sliderArray[0] = configuration.mode; + this.sliderArray[1] = configuration.scale; + this.sliderArray[2] = configuration.translateX; + this.sliderArray[3] = configuration.translateY; this.sliderArray[4] = configuration.distanceThreshold; this.sliderArray[5] = configuration.distanceScale; - //this.sliderArray[6] = configuration.distanceOffset; + this.sliderArray[6] = configuration.aliasingEnable; + this.sliderArray[7] = configuration.rampMin; - this.sliderArray[8] = configuration.outlineThreshold; - this.sliderArray[9] = configuration.outlineScale; - //this.sliderArray[10] = configuration.outlineOffset; + this.sliderArray[8] = configuration.subpixelEnable; + this.sliderArray[9] = configuration.subpixelStrength; + this.sliderArray[10] = configuration.subpixelOffset; + this.sliderArray[11] = configuration.rampMax; - this.sliderArray[12] = configuration.colorMix; - this.sliderArray[13] = configuration.colorScale; - this.sliderArray[14] = configuration.colorOffset; + this.sliderArray[12] = configuration.supersampleCount; + this.sliderArray[13] = configuration.supersampleOffset; + this.sliderArray[14] = configuration.outlineThreshold; + this.sliderArray[15] = configuration.outlineScale; device.queue.writeBuffer(this.frames[frameNumber].configurationBuffer, 0, this.sliderArray); + + return configuration; } - render(renderPass, frameNumber) + render(renderPass, frameNumber, sampleCount) { - renderPass.setPipeline(this.renderPipeline); + renderPass.setPipeline(this.renderPipelines[sampleCount]); renderPass.setIndexBuffer(this.buffer, "uint16"); renderPass.setBindGroup(0, this.viewBindGroup); renderPass.setBindGroup(1, this.perFontBindGroup); diff --git a/font.wgsl b/font.wgsl index bf8637a..4bea821 100644 --- a/font.wgsl +++ b/font.wgsl @@ -7,22 +7,25 @@ struct View { }; struct Config { + mode: f32, scale: f32, translateX: f32, translateY: f32, - padding0: f32, + distanceThreshold: f32, distanceScale: f32, - distanceOffset: f32, - padding1: f32, + aliasingEnable: f32, + rampMin: f32, + + subpixelEnable: f32, + subpixelStrength: f32, + subpixelOffset: f32, + rampMax: f32, + + supersampleCount: f32, + supersampleOffset: f32, outlineThreshold: f32, outlineScale: f32, - outlineOffset: f32, - padding2: f32, - colorMix: f32, - colorScale: f32, - colorOffset: f32, - padding3: f32, }; struct GlyphBuffer { @@ -55,7 +58,6 @@ struct VertexOutput { @location(0) @interpolate(linear, sample) texture: vec2f, //@location(0) texture: vec2f, @location(1) @interpolate(flat) glyphIndex: u32, - @location(2) foo: vec4f, }; const vertices = array( @@ -80,18 +82,9 @@ fn vertexMain(input: VertexInput) -> VertexOutput output.position = vec4f(screenPosition, 0.0, 1.0); output.texture = texture; output.glyphIndex = glyphIndex; - output.foo = vec4f(layoutBuffer[glyphIndex].position.xy, 0, 0); return output; } -fn blend(dst: vec4f, src: vec4f, distance: f32, threshold: f32, scale: f32, offset: f32) -> vec4f -{ - let scaledDistance = (distance - threshold) * scale + offset; - let alpha = clamp(scaledDistance, 0.0, 1.0); - let opacity = clamp(src.a * alpha - dst.a, 0.0, 1.0); - return opacity * src + (1.0 - opacity) * dst; -} - fn palette(t: f32) -> vec4f { let a = vec3f(0.5, 0.5, 0.5); @@ -111,46 +104,130 @@ fn aliasedLinearSaturation(distance: f32, threshold: f32, scale: f32) -> f32 let gradientLength = length(vec2f(dx, dy)); let thresholdWidth = scale * gradientLength; let aliased = saturate((distanceFromEdge / thresholdWidth) + 0.5); - return aliased; + return 1.0 - aliased; } -@fragment -fn fragmentMain(input: VertexOutput) -> @location(0) vec4f -{ - let p = glyphBuffer[input.glyphIndex].texPosition; - let s = glyphBuffer[input.glyphIndex].texSize; +struct FragmentOutput { + @location(0) color: vec4f, +}; - let base = textureSample(fontTexture, linearSampler, input.texture * s + p); +fn sampleAt(texture: vec2f) -> f32 +{ + let base = textureSample(fontTexture, linearSampler, texture); let distance = base.x; - let insideT = aliasedLinearSaturation(distance, - config.distanceThreshold, - config.distanceScale); + var t: f32; + if (config.aliasingEnable == 1.0) { + t = aliasedLinearSaturation(distance, config.distanceThreshold, config.distanceScale); + } else { + t = f32(distance > config.distanceThreshold); + } - let outlineT = aliasedLinearSaturation(distance, - config.outlineThreshold, - config.outlineScale); + return t; +} +fn sampleAt2(texture: vec2f, + threshold1: f32, scale1: f32, + threshold2: f32, scale2: f32) -> vec2f +{ + let base = textureSample(fontTexture, linearSampler, texture); + let distance = base.x; + + var t: vec2f; + if (config.aliasingEnable == 1.0) { + t.x = aliasedLinearSaturation(distance, threshold1, scale1); + t.y = aliasedLinearSaturation(distance, threshold2, scale2); + } else { + t.x = f32(distance > threshold1); + t.y = f32(distance > threshold2); + } + + return t; +} + +const offsets = array( + vec2f(0.125, 0.375), + vec2f(-0.125, -0.375), + vec2f(0.375, -0.125), + vec2f(-0.375, 0.125), +); + +fn supersampleColor(texture: vec2f, s: vec2f, p: vec2f) -> vec4f +{ let insideColor = vec4f(1, 1, 1, 1); let outlineColor = vec4f(0, 0, 0, 1); let outsideColor = vec4f(0, 0, 0, 0); - let outline = mix(outlineColor, outsideColor, outlineT); - return mix(insideColor, outline, insideT); + let dx = dpdx(texture.x * s.x); + let dy = dpdy(texture.y * s.y); - /* - let insideColorW = vec4f(1, 1, 1, 1); - let insideColorP = palette(distance * config.colorScale + config.colorOffset); - let insideColor = mix(insideColorW, insideColorP, config.colorMix); - let outlineColor = vec4f(0, 0, 0, 1); + let fdim = config.supersampleCount; + let idim = i32(floor(fdim / 2.0)); - var color = vec4f(0, 0, 0, 0); - color = blend(color, insideColor, distance, - config.distanceThreshold, config.distanceScale, config.distanceOffset); + var t = vec2f(0, 0); + if (fdim == 1) { + let coordinate = texture * s + p; + t = sampleAt2(coordinate, + config.distanceThreshold, config.distanceScale, + config.outlineThreshold, config.outlineScale); + } else { + for (var y = -idim; y <= idim; y++) { + for (var x = -idim; x <= idim; x++) { + //for (var i = 0; i < 4; i++) { + //let offset = vec2f(dx, dy) * offsets[i] * config.supersampleOffset; + let offset = vec2f(dx, dy) * vec2f(f32(x), f32(y)) * 0.1 * config.supersampleOffset; + let coordinate = texture * s + offset + p; + t += sampleAt2(coordinate, + config.distanceThreshold, config.distanceScale, + config.outlineThreshold, config.outlineScale); + //} + } + } + t *= (1.0 / (fdim * fdim)); + } - color = blend(color, outlineColor, distance, - config.distanceThreshold - config.outlineThreshold, - config.outlineScale, config.outlineOffset); - return color; - */ + let color = mix(outsideColor, outlineColor, t.y); + return mix(color, insideColor, t.x); +} + +fn subpixelColor(texture: vec2f, s: vec2f, p: vec2f) -> vec4f +{ + let dx = dpdx(texture.x * s.x * 1/3); + let dy = dpdy(texture.y * s.y); + + var subpixelColor = vec3f(0, 0, 0); + let insideColor = vec4f(1, 1, 1, 1); + let outsideColor = vec4f(0, 0, 0, 0); + + if (config.subpixelEnable == 0) { + let coordinate = texture * s + p; + let t = sampleAt(coordinate); + return mix(outsideColor, insideColor, t); + } else { + for (var x = -1; x <= 1; x += 1) { + let offset = vec2f(dx * f32(x) * config.subpixelOffset, 0); + let coordinate = texture * s + offset + p; + subpixelColor[(x + 1)] = sampleAt(coordinate); + } + let mono = (subpixelColor.x + subpixelColor.y + subpixelColor.z) / 3; + let subpixelMix = mix(vec3f(mono, mono, mono), subpixelColor.xyz, config.subpixelStrength); + return vec4f(subpixelMix, length(subpixelMix)); + } +} + +@fragment +fn fragmentMain(input: VertexOutput) -> FragmentOutput +{ + let s = glyphBuffer[input.glyphIndex].texSize; + let p = glyphBuffer[input.glyphIndex].texPosition; + + var output: FragmentOutput; + + if (config.mode == 0) { + output.color = subpixelColor(input.texture, s, p); + } else { + output.color = supersampleColor(input.texture, s, p); + } + + return output; } diff --git a/fontSliders.js b/fontSliders.js index a99ddc1..b8960c3 100644 --- a/fontSliders.js +++ b/fontSliders.js @@ -1,23 +1,37 @@ import { Slider } from "./slider.js" +import { Select } from "./select.js" +import { RowHeader } from "./row-header.js" function makeSliders() { const sliders = { + subpixelOptions: new RowHeader("global options"), + mode: new Select("mode", ["subpixel", "supersampling"]), scale: new Slider("scale", 0.0, 0.01), translateX: new Slider("translate x", -1, 1), translateY: new Slider("translate y", -1, 1), + multisampleCount: new Slider("multisampleCount", 1, 4, 1), - distanceThreshold: new Slider("dist. threshold", 0.25, 0.75), - distanceScale: new Slider("distance scale", 0.0, 2.0), - //distanceOffset: new Slider("distance offset", -20.0, 20.0), + distanceThreshold: new Slider("distance threshold", 0.25, 0.75), + distanceScale: new Slider("distance scale", 0.0, 20.0), + aliasingEnable: new Slider("aliasing enable", 0, 1, 1), + //rampMin: new Slider("ramp min", 0, 1.0), + //rampMax: new Slider("ramp max", 0, 1.0), + subpixelOptions: new RowHeader("subpixel mode options"), + subpixelEnable: new Slider("subpixel enable", 0, 1, 1), + subpixelStrength: new Slider("subpixel strength", 0.0, 1.0), + subpixelOffset: new Slider("subpixel offset", 0.0, 10.0), + + supersamplingOptions: new RowHeader("supersampling mode options"), + supersampleCount: new Slider("supersample count", 1.0, 15.0, 7), + supersampleOffset: new Slider("supersample offset", 0.0, 10.0), outlineThreshold: new Slider("outline threshold", 0.0, 1.0), - outlineScale: new Slider("outline scale", 0.0, 50.0), - //outlineOffset: new Slider("outline offset", -20.0, 20.0), + outlineScale: new Slider("outline scale", 0.0, 20.0), - colorMix: new Slider("color mix", 0.0, 1.0), - colorScale: new Slider("color scale", 0.0, 10.0), - colorOffset: new Slider("color offset", 0.0, 3.1415), + //colorMix: new Slider("color mix", 0.0, 1.0), + //colorScale: new Slider("color scale", 0.0, 10.0), + //colorOffset: new Slider("color offset", 0.0, 3.1415), }; /* diff --git a/index.html b/index.html index 0c92e17..346cef4 100644 --- a/index.html +++ b/index.html @@ -8,15 +8,71 @@ :root { color-scheme: light dark; } - div, span { + label { font: 0.8rem sans-serif; } - span { - padding-bottom: 0.1rem; + code { + font: 0.85rem monospace; } + control-root { + position: fixed; + left: 0; + top: 0; + right: 0; + height: 0; + background: #eee; + } + control-main { + width: 350px; + float: right; + margin-right: 1rem; + background: #444; + } + control-grid { + display: grid; + grid-template-columns: auto auto minmax(4.25em, auto); + column-gap: 0.5rem; + align-items: center; + } + row { + display: contents; + } + row > *:first-child { + margin-left: 0.5rem; + justify-self: left; + } + row > *:last-child { + margin-right: 0.5rem; + justify-self: right; + } + row-header { + font: 0.9rem sans-serif; + font-weight: bold; + justify-self: center !important; + margin-top: 0.2em; + } + row-separator, row-header { + grid-column: 1 / span 3; + } + row-separator:not(:last-child) { + border-bottom: 1px solid #777; + } + row-header + row-separator { + border-bottom: 2px solid #888 !important; + } + select, option { font: 0.8rem sans-serif; } + input[type="range"] { + height: 1rem; + width: calc(100% - 0.5em); + } + .span-2 { + grid-column: span 2; + width: calc(100% - 0.5em); + } + canvas { position: absolute; left: 0; @@ -26,115 +82,17 @@ width: 100%; height: 100%; } - - input[type="range"] { - height: 0.9rem; - width: 70%; - } - - .control-root { - position: fixed; - left: 0; - top: 0; - right: 0; - height: 0; - background: #eee; - } - .control-main { - width: 340px; - float: right; - margin-right: 15px; - background: #444; - } - .label { - width: calc(35% - 0.3rem); - float: left; - clear: left; - overflow: hidden; - margin-left: 0.3rem; - margin-top: 0.25rem; - } - .value { - width: calc(65% - 0.2rem); - float: left; - margin-top: 0.1rem; - margin-right: 0.2rem; - } - .row:not(:last-child) { - border-bottom: 1px solid #999; - } - .row { - height: 1.5rem; - } - .code, code { - font: 0.85rem monospace; - padding-top: 0.15rem; - } - .float3 { - width: 30%; - display: inline-block; - text-align: right; - } - .float2 { - width: 40%; - display: inline-block; - text-align: right; - } - .range-span { - float: right; - }
-