// Copyright 2025 The Ebitengine Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package vector import ( "sync" "github.com/hajimehoshi/ebiten/v2" ) // The implementation is based on the following article: // https://medium.com/@evanwallace/easy-scalable-text-rendering-on-the-gpu-c3f4d782c5ac // These values are protected by cacheM. var ( stencilBufferFillShader *ebiten.Shader stencilBufferBezierShader *ebiten.Shader stencilBufferNonZeroShader *ebiten.Shader stencilBufferNonZeroAAShader *ebiten.Shader stencilBufferEvenOddShader *ebiten.Shader stencilBufferEvenOddAAShader *ebiten.Shader stencilBufferM sync.Mutex ) func ensureStencilBufferShaders() (*ebiten.Shader, error) { stencilBufferM.Lock() defer stencilBufferM.Unlock() if stencilBufferFillShader != nil { return stencilBufferFillShader, nil } s, err := ebiten.NewShader([]byte(stencilBufferFillShaderSrc)) if err != nil { return nil, err } stencilBufferFillShader = s return stencilBufferFillShader, err } func ensureStencilBufferBezierShader() (*ebiten.Shader, error) { stencilBufferM.Lock() defer stencilBufferM.Unlock() if stencilBufferBezierShader != nil { return stencilBufferBezierShader, nil } s, err := ebiten.NewShader([]byte(stencilBufferBezierShaderSrc)) if err != nil { return nil, err } stencilBufferBezierShader = s return stencilBufferBezierShader, nil } func ensureStencilBufferNonZeroShader(antialias bool) (*ebiten.Shader, error) { stencilBufferM.Lock() defer stencilBufferM.Unlock() if antialias { if stencilBufferNonZeroAAShader != nil { return stencilBufferNonZeroAAShader, nil } s, err := ebiten.NewShader([]byte(stencilBufferNonZeroAAShaderSrc)) if err != nil { return nil, err } stencilBufferNonZeroAAShader = s return stencilBufferNonZeroAAShader, nil } if stencilBufferNonZeroShader != nil { return stencilBufferNonZeroShader, nil } s, err := ebiten.NewShader([]byte(stencilBufferNonZeroShaderSrc)) if err != nil { return nil, err } stencilBufferNonZeroShader = s return stencilBufferNonZeroShader, nil } func ensureStencilBufferEvenOddShader(antialias bool) (*ebiten.Shader, error) { stencilBufferM.Lock() defer stencilBufferM.Unlock() if antialias { if stencilBufferEvenOddAAShader != nil { return stencilBufferEvenOddAAShader, nil } s, err := ebiten.NewShader([]byte(stencilBufferEvenOddAAShaderSrc)) if err != nil { return nil, err } stencilBufferEvenOddAAShader = s return stencilBufferEvenOddAAShader, nil } if stencilBufferEvenOddShader != nil { return stencilBufferEvenOddShader, nil } s, err := ebiten.NewShader([]byte(stencilBufferEvenOddShaderSrc)) if err != nil { return nil, err } stencilBufferEvenOddShader = s return stencilBufferEvenOddShader, nil } //ebitengine:shadersource const stencilBufferFillShaderSrc = `//kage:unit pixels package main func Fragment(dstPos vec4, srcPos vec2, color vec4, custom vec4) vec4 { v := 1.0 / 255.0 if frontfacing() { v *= 16 } return v * color } ` //ebitengine:shadersource const stencilBufferBezierShaderSrc = `//kage:unit pixels package main func Fragment(dstPos vec4, srcPos vec2, color vec4, custom vec4) vec4 { // Loop-Blinn algorithm. // https://developer.nvidia.com/gpugems/gpugems3/part-iv-image-effects/chapter-25-rendering-vector-art-gpu uv := custom.xy v := clamp(-sign(uv.x * uv.x - uv.y), 0, 1) * 1.0/255.0 // This is opposite to the fill shader, especially for the non-zero fill rule. if !frontfacing() { v *= 16 } return v * color } ` //ebitengine:shadersource const stencilBufferNonZeroShaderSrc = `//kage:unit pixels package main func round(x float) float { return floor(x + 0.5) } func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 { c := imageSrc0UnsafeAt(srcPos) r := int(round(c.r*255)) w := abs((r >> 4) - (r & 0x0F)) v := min(float(w), 1) return v * color } ` //ebitengine:shadersource const stencilBufferNonZeroAAShaderSrc = `//kage:unit pixels package main func round(x vec4) vec4 { return floor(x + 0.5) } func Fragment(dstPos vec4, srcPos vec2, color vec4, custom vec4) vec4 { c0 := imageSrc0UnsafeAt(srcPos) // imageSrc1UnsafeAt uses the offset info, which would prevent batching. // Use a custom offset instead. c1 := imageSrc0UnsafeAt(srcPos + custom.xy) ci0 := ivec4(round(c0*255)) ci1 := ivec4(round(c1*255)) w0 := abs((ci0 >> 4) - (ci0 & 0x0F)) w1 := abs((ci1 >> 4) - (ci1 & 0x0F)) v0 := min(vec4(w0), 1) v1 := min(vec4(w1), 1) return (dot(v0, vec4(1.0/8.0)) + dot(v1, vec4(1.0/8.0))) * color } ` //ebitengine:shadersource const stencilBufferEvenOddShaderSrc = `//kage:unit pixels package main func round(x float) float { return floor(x + 0.5) } func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 { c := imageSrc0UnsafeAt(srcPos) r := int(round(c.r*255)) v := abs((r >> 4) - (r & 0x0F)) return float(v % 2) * color } ` //ebitengine:shadersource const stencilBufferEvenOddAAShaderSrc = `//kage:unit pixels package main func round(x vec4) vec4 { return floor(x + 0.5) } func Fragment(dstPos vec4, srcPos vec2, color vec4, custom vec4) vec4 { c0 := imageSrc0UnsafeAt(srcPos) // imageSrc1UnsafeAt uses the offset info, which would prevent batching. // Use a custom offset instead. c1 := imageSrc0UnsafeAt(srcPos + custom.xy) ci0 := ivec4(round(c0*255)) ci1 := ivec4(round(c1*255)) w0 := abs((ci0 >> 4) - (ci0 & 0x0F)) w1 := abs((ci1 >> 4) - (ci1 & 0x0F)) v0 := vec4(w0 % 2) v1 := vec4(w1 % 2) return (dot(v0, vec4(1.0/8.0)) + dot(v1, vec4(1.0/8.0))) * color } `