diff --git a/image.go b/image.go index bea6e4374..0774f64e5 100644 --- a/image.go +++ b/image.go @@ -21,6 +21,7 @@ import ( "github.com/hajimehoshi/ebiten/v2/internal/affine" "github.com/hajimehoshi/ebiten/v2/internal/atlas" + "github.com/hajimehoshi/ebiten/v2/internal/builtinshader" "github.com/hajimehoshi/ebiten/v2/internal/graphics" "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" "github.com/hajimehoshi/ebiten/v2/internal/ui" @@ -233,7 +234,22 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) { srcs := [graphics.ShaderImageCount]*ui.Image{img.image} - i.image.DrawTriangles(srcs, vs, is, colorm, mode, filter, graphicsdriver.AddressUnsafe, i.adjustedRegion(), graphicsdriver.Region{}, [graphics.ShaderImageCount - 1][2]float32{}, nil, nil, false, canSkipMipmap(options.GeoM, filter)) + useColorM := !colorm.IsIdentity() + var shader *ui.Shader + var uniforms [][]float32 + if useColorM { + s := builtinShader(graphicsdriver.Filter(filter), graphicsdriver.AddressUnsafe) + var body [16]float32 + var translation [4]float32 + colorm.Elements(body[:], translation[:]) + uniforms = s.convertUniforms(map[string]interface{}{ + builtinshader.UniformColorMBody: body[:], + builtinshader.UniformColorMTranslation: translation[:], + }) + shader = s.shader + } + + i.image.DrawTriangles(srcs, vs, is, affine.ColorMIdentity{}, mode, filter, graphicsdriver.AddressUnsafe, i.adjustedRegion(), graphicsdriver.Region{}, [graphics.ShaderImageCount - 1][2]float32{}, shader, uniforms, false, canSkipMipmap(options.GeoM, filter)) } // Vertex represents a vertex passed to DrawTriangles. @@ -398,7 +414,22 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o srcs := [graphics.ShaderImageCount]*ui.Image{img.image} - i.image.DrawTriangles(srcs, vs, is, colorm, mode, filter, address, i.adjustedRegion(), sr, [graphics.ShaderImageCount - 1][2]float32{}, nil, nil, options.FillRule == EvenOdd, false) + useColorM := !colorm.IsIdentity() + var shader *ui.Shader + var uniforms [][]float32 + if useColorM { + s := builtinShader(graphicsdriver.Filter(filter), graphicsdriver.Address(address)) + var body [16]float32 + var translation [4]float32 + colorm.Elements(body[:], translation[:]) + uniforms = s.convertUniforms(map[string]interface{}{ + builtinshader.UniformColorMBody: body[:], + builtinshader.UniformColorMTranslation: translation[:], + }) + shader = s.shader + } + + i.image.DrawTriangles(srcs, vs, is, affine.ColorMIdentity{}, mode, filter, address, i.adjustedRegion(), sr, [graphics.ShaderImageCount - 1][2]float32{}, shader, uniforms, options.FillRule == EvenOdd, false) } // DrawTrianglesShaderOptions represents options for DrawTrianglesShader. diff --git a/internal/builtinshader/shader.go b/internal/builtinshader/shader.go new file mode 100644 index 000000000..4ff46857e --- /dev/null +++ b/internal/builtinshader/shader.go @@ -0,0 +1,155 @@ +// Copyright 2022 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 builtinshader + +import ( + "bytes" + "fmt" + "sync" + "text/template" + + "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" +) + +const ( + UniformColorMBody = "ColorMBody" + UniformColorMTranslation = "ColorMTranslation" +) + +type key struct { + Filter graphicsdriver.Filter + Address graphicsdriver.Address + UseColorM bool +} + +var ( + shaders = map[key][]byte{} + shadersM sync.Mutex +) + +var tmpl = template.Must(template.New("tmpl").Parse(`package main + +{{if .UseColorM}} +var ColorMBody mat4 +var ColorMTranslation vec4 +{{end}} + +{{if eq .Address .AddressRepeat}} +func adjustTexelForAddressRepeat(p vec2) vec2 { + origin, size := imageSrcRegionOnTexture() + return mod(p - origin, size) + origin +} +{{end}} + +func Fragment(position vec4, texCoord vec2, color vec4) vec4 { +{{if eq .Filter .FilterNearest}} +{{if eq .Address .AddressUnsafe}} + clr := imageSrc0UnsafeAt(texCoord) +{{else if eq .Address .AddressClampToZero}} + clr := imageSrc0At(texCoord) +{{else if eq .Address .AddressRepeat}} + clr := imageSrc0At(adjustTexelForAddressRepeat(texCoord)) +{{end}} +{{else if eq .Filter .FilterLinear}} + sourceSize := imageSrcTextureSize() + texelSize := 1 / sourceSize + + // Shift 1/512 [texel] to avoid the tie-breaking issue (#1212). + // As all the vertex positions are aligned to 1/16 [pixel], this shiting should work in most cases. + p0 := texCoord - texelSize/2 + texelSize/512 + p1 := texCoord + texelSize/2 + texelSize/512 + +{{if eq .Address .AddressRpeat}} + p0 = adjustTexelForAddressRepeat(p0) + p1 = adjustTexelForAddressRepeat(p1) +{{end}} + +{{if eq .Address .AddressUnsafe}} + c0 := imageSrc0UnsafeAt(p0) + c1 := imageSrc0UnsafeAt(vec2(p1.x, p0.y)) + c2 := imageSrc0UnsafeAt(vec2(p0.x, p1.y)) + c3 := imageSrc0UnsafeAt(p1) +{{else}} + c0 := imageSrc0At(p0) + c1 := imageSrc0At(vec2(p1.x, p0.y)) + c2 := imageSrc0At(vec2(p0.x, p1.y)) + c3 := imageSrc0At(p1) +{{end}} + + rate := fract(p0 * sourceSize) + clr := mix(mix(c0, c1, rate.x), mix(c2, c3, rate.x), rate.y) +{{end}} + +{{if .UseColorM}} + // Un-premultiply alpha. + // When the alpha is 0, 1-sign(alpha) is 1.0, which means division does nothing. + clr.rgb /= clr.a + (1-sign(clr.a)) + // Apply the clr matrix or scale. + clr = (ColorMBody * clr) + ColorMTranslation + // Premultiply alpha + clr.rgb *= clr.a + // Do not apply the color scale assuming the scale is (1, 1, 1, 1). + // Clamp the output. + clr.rgb = min(clr.rgb, clr.a) +{{else}} + // Apply the color scale. + clr *= color +{{end}} + + return clr +} + +`)) + +// Shader returns the built-in shader based on the given parameters. +// +// The returned shader always uses a color matrix so far. +func Shader(filter graphicsdriver.Filter, address graphicsdriver.Address) []byte { + shadersM.Lock() + defer shadersM.Unlock() + + // Now UseColorM is always true. When UseColorM is true, the color scale in vertices is not used. + // In the built-in shaders, the color scale is modified at the vertex shader (#1996), + // and this modification cannot be represented in a Kage program. + // A custom vertex shader in Kage (#2209), or changing the interpretation of scales (#2365) would solve the issue. + const useColorM = true + + k := key{ + Filter: filter, + Address: address, + UseColorM: useColorM, + } + if s, ok := shaders[k]; ok { + return s + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, map[string]interface{}{ + "Filter": filter, + "FilterNearest": graphicsdriver.FilterNearest, + "FilterLinear": graphicsdriver.FilterLinear, + "Address": address, + "AddressUnsafe": graphicsdriver.AddressUnsafe, + "AddressClampToZero": graphicsdriver.AddressClampToZero, + "AddressRepeat": graphicsdriver.AddressRepeat, + "UseColorM": useColorM, + }); err != nil { + panic(fmt.Sprintf("builtinshader: tmpl.Execute failed: %v", err)) + } + + b := buf.Bytes() + shaders[k] = b + return b +} diff --git a/shader.go b/shader.go index 950d0f550..dcfd5956d 100644 --- a/shader.go +++ b/shader.go @@ -15,7 +15,12 @@ package ebiten import ( + "fmt" + "sync" + + "github.com/hajimehoshi/ebiten/v2/internal/builtinshader" "github.com/hajimehoshi/ebiten/v2/internal/graphics" + "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" "github.com/hajimehoshi/ebiten/v2/internal/ui" ) @@ -51,3 +56,34 @@ func (s *Shader) Dispose() { func (s *Shader) convertUniforms(uniforms map[string]interface{}) [][]float32 { return s.shader.ConvertUniforms(uniforms) } + +type builtinShaderKey struct { + filter graphicsdriver.Filter + address graphicsdriver.Address +} + +var ( + builtinShaders = map[builtinShaderKey]*Shader{} + builtinShadersM sync.Mutex +) + +func builtinShader(filter graphicsdriver.Filter, address graphicsdriver.Address) *Shader { + builtinShadersM.Lock() + defer builtinShadersM.Unlock() + + key := builtinShaderKey{ + filter: filter, + address: address, + } + if s, ok := builtinShaders[key]; ok { + return s + } + + src := builtinshader.Shader(filter, address) + s, err := NewShader(src) + if err != nil { + panic(fmt.Sprintf("ebiten: NewShader for a built-in shader failed: %v", err)) + } + builtinShaders[key] = s + return s +}