Kage Shaders (//kage:unit pixels) & ebiten/v2/text/v2 Typography Reference #
This module covers Ebitengine’s Kage shading language using pixel-unit coordinates (//kage:unit pixels) for 2D lighting, CRT post-processing, and damage hit-flashes, alongside modern typography rendering with github.com/hajimehoshi/ebiten/v2/text/v2.
1. Kage Shaders (//kage:unit pixels) #
Always declare //kage:unit pixels at the top of Kage shader files so imageSrc0At(srcPos) and uniform coordinates operate in predictable virtual pixel coordinates rather than normalized texture atlas UV fractions.
1.1 Sprite Damage Hit-Flash & Silhouette Tint Shader (hit_flash.kage) #
Blends a sprite’s sampled premultiplied pixel toward a solid flash color (e.g. pure white or red on hit) while preserving the original pixel’s alpha silhouette:
//go:build ignore
//kage:unit pixels
package main
var FlashAmount float // [0.0, 1.0]
var FlashColor vec3 // Unpremultiplied RGB in [0.0, 1.0], e.g. vec3(1.0, 1.0, 1.0)
func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
px := imageSrc0At(srcPos) * color
if px.a <= 0.0 {
return vec4(0.0)
}
// Preserve premultiplied alpha invariant: RGB <= A
targetRGB := FlashColor * px.a
mixedRGB := mix(px.rgb, targetRGB, clamp(FlashAmount, 0.0, 1.0))
return vec4(mixedRGB, px.a)
}1.2 2D Radial Point Lighting & Vignette Shader (lighting.kage) #
Applies dynamic point-light attenuation and ambient shadow tinting across any 2D scene (top-down dungeons, tactics maps, shmups, or side-scrollers):
//go:build ignore
//kage:unit pixels
package main
var LightPos vec2 // Light position in virtual screen pixels
var LightRadius float // Falloff radius in virtual pixels
var AmbientColor vec3 // Minimum ambient RGB illumination (e.g., vec3(0.15, 0.12, 0.25))
var LightColor vec3 // Point light RGB tint (e.g., vec3(1.0, 0.85, 0.55))
func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
base := imageSrc0At(srcPos) * color
if base.a <= 0.0 {
return vec4(0.0)
}
// Calculate distance from current screen pixel to light center
origin := imageDstOrigin()
localPos := dstPos.xy - origin
dist := distance(localPos, LightPos)
// Smooth quadratic attenuation
attenuation := clamp(1.0-(dist/LightRadius), 0.0, 1.0)
attenuation = attenuation * attenuation
illumination := AmbientColor + LightColor*attenuation
return vec4(base.rgb*illumination, base.a)
}1.3 CRT Scanline & Curvature Post-Processing Shader (crt.kage) #
//go:build ignore
//kage:unit pixels
package main
var ScreenSize vec2
var ScanlineIntensity float // e.g., 0.18
func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
px := imageSrc0At(srcPos) * color
// Apply horizontal scanline darkening on every odd virtual pixel row
rowPhase := sin(srcPos.y * 3.14159265)
scanline := 1.0 - ScanlineIntensity*(0.5-0.5*rowPhase)
return vec4(px.rgb*scanline, px.a)
}1.4 Compiling and Invoking Kage Shaders with Fail-Fast Validation #
Compile shaders once during Boot initialization and reuse the pre-allocated uniform map and ebiten.DrawRectShaderOptions in Draw():
package render
import (
"fmt"
"github.com/hajimehoshi/ebiten/v2"
)
type ShaderPass struct {
shader *ebiten.Shader
op ebiten.DrawRectShaderOptions
}
func NewShaderPass(kageSource []byte) (*ShaderPass, error) {
if len(kageSource) == 0 {
return nil, fmt.Errorf("render: empty Kage shader source")
}
sh, err := ebiten.NewShader(kageSource)
if err != nil {
return nil, fmt.Errorf("render: compile Kage shader: %w", err)
}
sp := &ShaderPass{shader: sh}
sp.op.Uniforms = make(map[string]any, 8)
return sp, nil
}
func (sp *ShaderPass) DrawSpriteWithFlash(dst, src *ebiten.Image, x, y float64, flashAmount float32) {
b := src.Bounds()
sp.op.GeoM.Reset()
sp.op.GeoM.Translate(x, y)
sp.op.Images[0] = src
sp.op.Uniforms["FlashAmount"] = flashAmount
sp.op.Uniforms["FlashColor"] = []float32{1.0, 1.0, 1.0}
dst.DrawRectShader(b.Dx(), b.Dy(), sp.shader, &sp.op)
}2. Modern Typography with ebiten/v2/text/v2 #
CRITICAL RULE: Never use
ebitenutil.DebugPrintorebitenutil.DebugPrintAtfor user-facing game UI, and never import the deprecatedgithub.com/hajimehoshi/ebiten/v2/text(v1) package. Always usegithub.com/hajimehoshi/ebiten/v2/text/v2.
2.1 Loading Embedded TTF/OTF Fonts (text.NewGoTextFaceSource) #
Load the text.GoTextFaceSource once from embed.FS at startup and fail fast if the font binary is invalid:
package ui
import (
"bytes"
"fmt"
"image/color"
"io/fs"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/text/v2"
)
type FontBank struct {
source *text.GoTextFaceSource
BodyFace *text.GoTextFace
TitleFace *text.GoTextFace
drawOp text.DrawOptions
}
func NewFontBank(assetsFS fs.FS, fontPath string, bodySize, titleSize float64) (*FontBank, error) {
raw, err := fs.ReadFile(assetsFS, fontPath)
if err != nil {
return nil, fmt.Errorf("ui: read embedded font %q: %w", fontPath, err)
}
src, err := text.NewGoTextFaceSource(bytes.NewReader(raw))
if err != nil {
return nil, fmt.Errorf("ui: parse TTF/OTF font source %q: %w", fontPath, err)
}
if bodySize <= 0 || titleSize <= 0 {
return nil, fmt.Errorf("ui: font sizes must be > 0 (body=%.1f, title=%.1f)", bodySize, titleSize)
}
return &FontBank{
source: src,
BodyFace: &text.GoTextFace{
Source: src,
Size: bodySize,
},
TitleFace: &text.GoTextFace{
Source: src,
Size: titleSize,
},
}, nil
}2.2 Aligned & Shadowed Text Rendering (text.Draw) #
text/v2 supports native horizontal/vertical alignment (PrimaryAlign, SecondaryAlign) and multiline line spacing (LineSpacing) directly via text.DrawOptions:
// DrawAligned renders crisp text with a 1px drop shadow and zero heap allocations in Draw().
func (fb *FontBank) DrawAligned(
screen *ebiten.Image,
str string,
face *text.GoTextFace,
x, y float64,
hAlign, vAlign text.Align,
fgColor, shadowColor color.Color,
) {
// 1. Draw drop shadow at (+1, +1)
fb.drawOp.GeoM.Reset()
fb.drawOp.GeoM.Translate(x+1, y+1)
fb.drawOp.ColorScale.Reset()
fb.drawOp.ColorScale.ScaleWithColor(shadowColor)
fb.drawOp.LayoutOptions.PrimaryAlign = hAlign
fb.drawOp.LayoutOptions.SecondaryAlign = vAlign
fb.drawOp.LayoutOptions.LineSpacing = face.Size * 1.25
text.Draw(screen, str, face, &fb.drawOp)
// 2. Draw foreground text at (x, y)
fb.drawOp.GeoM.Reset()
fb.drawOp.GeoM.Translate(x, y)
fb.drawOp.ColorScale.Reset()
fb.drawOp.ColorScale.ScaleWithColor(fgColor)
text.Draw(screen, str, face, &fb.drawOp)
}