mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-24 18:58:54 +01:00
internal/ui: refactoring: move the screen rendering logic to ebiten package
Updates #2046
This commit is contained in:
parent
08e6f5af86
commit
4bd3a9ef8f
100
gameforui.go
100
gameforui.go
@ -15,21 +15,84 @@
|
||||
package ebiten
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/ui"
|
||||
)
|
||||
|
||||
const screenShaderSrc = `package main
|
||||
|
||||
func Fragment(position vec4, texCoord vec2, color vec4) vec4 {
|
||||
// TODO: Calculate the scale in the shader after pixels become the main unit in shaders (#1431)
|
||||
_, dr := imageDstRegionOnTexture()
|
||||
_, sr := imageSrcRegionOnTexture()
|
||||
scale := (imageDstTextureSize() * dr) / (imageSrcTextureSize() * sr)
|
||||
|
||||
sourceSize := imageSrcTextureSize()
|
||||
// texelSize is one pixel size in texel sizes.
|
||||
texelSize := 1 / sourceSize
|
||||
halfScaledTexelSize := texelSize / 2 / scale
|
||||
|
||||
// Shift 1/512 [texel] to avoid the tie-breaking issue.
|
||||
pos := texCoord
|
||||
p0 := pos - halfScaledTexelSize + (texelSize / 512)
|
||||
p1 := pos + halfScaledTexelSize + (texelSize / 512)
|
||||
|
||||
// Texels must be in the source rect, so it is not necessary to check.
|
||||
c0 := imageSrc0UnsafeAt(p0)
|
||||
c1 := imageSrc0UnsafeAt(vec2(p1.x, p0.y))
|
||||
c2 := imageSrc0UnsafeAt(vec2(p0.x, p1.y))
|
||||
c3 := imageSrc0UnsafeAt(p1)
|
||||
|
||||
// p is the p1 value in one pixel assuming that the pixel's upper-left is (0, 0) and the lower-right is (1, 1).
|
||||
p := fract(p1 * sourceSize)
|
||||
|
||||
// rate indicates how much the 4 colors are mixed. rate is in between [0, 1].
|
||||
//
|
||||
// 0 <= p <= 1/Scale: The rate is in between [0, 1]
|
||||
// 1/Scale < p: Don't care. Adjacent colors (e.g. c0 vs c1 in an X direction) should be the same.
|
||||
rate := clamp(p*scale, 0, 1)
|
||||
return mix(mix(c0, c1, rate.x), mix(c2, c3, rate.x), rate.y)
|
||||
}
|
||||
`
|
||||
|
||||
var screenFilterEnabled = int32(1)
|
||||
|
||||
func isScreenFilterEnabled() bool {
|
||||
return atomic.LoadInt32(&screenFilterEnabled) != 0
|
||||
}
|
||||
|
||||
func setScreenFilterEnabled(enabled bool) {
|
||||
v := int32(0)
|
||||
if enabled {
|
||||
v = 1
|
||||
}
|
||||
atomic.StoreInt32(&screenFilterEnabled, v)
|
||||
}
|
||||
|
||||
type gameForUI struct {
|
||||
game Game
|
||||
offscreen *Image
|
||||
screen *Image
|
||||
screenShader *Shader
|
||||
}
|
||||
|
||||
func newGameForUI(game Game) *gameForUI {
|
||||
return &gameForUI{
|
||||
g := &gameForUI{
|
||||
game: game,
|
||||
}
|
||||
|
||||
s, err := NewShader([]byte(screenShaderSrc))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("ebiten: compiling the screen shader failed: %v", err))
|
||||
}
|
||||
g.screenShader = s
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
func (c *gameForUI) NewOffscreenImage(width, height int) *ui.Image {
|
||||
@ -51,6 +114,16 @@ func (c *gameForUI) NewOffscreenImage(width, height int) *ui.Image {
|
||||
return c.offscreen.image
|
||||
}
|
||||
|
||||
func (c *gameForUI) NewScreenImage(width, height int) *ui.Image {
|
||||
if c.screen != nil {
|
||||
c.screen.Dispose()
|
||||
c.screen = nil
|
||||
}
|
||||
|
||||
c.screen = newImage(image.Rect(0, 0, width, height), atlas.ImageTypeScreen)
|
||||
return c.screen.image
|
||||
}
|
||||
|
||||
func (c *gameForUI) Layout(outsideWidth, outsideHeight int) (int, int) {
|
||||
return c.game.Layout(outsideWidth, outsideHeight)
|
||||
}
|
||||
@ -59,6 +132,29 @@ func (c *gameForUI) Update() error {
|
||||
return c.game.Update()
|
||||
}
|
||||
|
||||
func (c *gameForUI) Draw() {
|
||||
func (c *gameForUI) DrawOffscreen() {
|
||||
c.game.Draw(c.offscreen)
|
||||
}
|
||||
|
||||
func (g *gameForUI) DrawScreen(scale, offsetX, offsetY float64) {
|
||||
switch {
|
||||
case !isScreenFilterEnabled(), math.Floor(scale) == scale:
|
||||
op := &DrawImageOptions{}
|
||||
op.GeoM.Scale(scale, scale)
|
||||
op.GeoM.Translate(offsetX, offsetY)
|
||||
g.screen.DrawImage(g.offscreen, op)
|
||||
case scale < 1:
|
||||
op := &DrawImageOptions{}
|
||||
op.GeoM.Scale(scale, scale)
|
||||
op.GeoM.Translate(offsetX, offsetY)
|
||||
op.Filter = FilterLinear
|
||||
g.screen.DrawImage(g.offscreen, op)
|
||||
default:
|
||||
op := &DrawRectShaderOptions{}
|
||||
op.Images[0] = g.offscreen
|
||||
op.GeoM.Scale(scale, scale)
|
||||
op.GeoM.Translate(offsetX, offsetY)
|
||||
w, h := g.offscreen.Size()
|
||||
g.screen.DrawRectShader(w, h, g.screenShader, op)
|
||||
}
|
||||
}
|
||||
|
@ -15,12 +15,10 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/buffered"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/clock"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/debug"
|
||||
@ -30,63 +28,18 @@ import (
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/mipmap"
|
||||
)
|
||||
|
||||
const screenShaderSrc = `package main
|
||||
|
||||
func Fragment(position vec4, texCoord vec2, color vec4) vec4 {
|
||||
// TODO: Calculate the scale in the shader after pixels become the main unit in shaders (#1431)
|
||||
_, dr := imageDstRegionOnTexture()
|
||||
_, sr := imageSrcRegionOnTexture()
|
||||
scale := (imageDstTextureSize() * dr) / (imageSrcTextureSize() * sr)
|
||||
|
||||
sourceSize := imageSrcTextureSize()
|
||||
// texelSize is one pixel size in texel sizes.
|
||||
texelSize := 1 / sourceSize
|
||||
halfScaledTexelSize := texelSize / 2 / scale
|
||||
|
||||
// Shift 1/512 [texel] to avoid the tie-breaking issue.
|
||||
pos := texCoord
|
||||
p0 := pos - halfScaledTexelSize + (texelSize / 512)
|
||||
p1 := pos + halfScaledTexelSize + (texelSize / 512)
|
||||
|
||||
// Texels must be in the source rect, so it is not necessary to check.
|
||||
c0 := imageSrc0UnsafeAt(p0)
|
||||
c1 := imageSrc0UnsafeAt(vec2(p1.x, p0.y))
|
||||
c2 := imageSrc0UnsafeAt(vec2(p0.x, p1.y))
|
||||
c3 := imageSrc0UnsafeAt(p1)
|
||||
|
||||
// p is the p1 value in one pixel assuming that the pixel's upper-left is (0, 0) and the lower-right is (1, 1).
|
||||
p := fract(p1 * sourceSize)
|
||||
|
||||
// rate indicates how much the 4 colors are mixed. rate is in between [0, 1].
|
||||
//
|
||||
// 0 <= p <= 1/Scale: The rate is in between [0, 1]
|
||||
// 1/Scale < p: Don't care. Adjacent colors (e.g. c0 vs c1 in an X direction) should be the same.
|
||||
rate := clamp(p*scale, 0, 1)
|
||||
return mix(mix(c0, c1, rate.x), mix(c2, c3, rate.x), rate.y)
|
||||
}
|
||||
`
|
||||
|
||||
var (
|
||||
screenShader *Shader
|
||||
NearestFilterShader = &Shader{shader: mipmap.NearestFilterShader}
|
||||
LinearFilterShader = &Shader{shader: mipmap.LinearFilterShader}
|
||||
)
|
||||
|
||||
func init() {
|
||||
{
|
||||
ir, err := graphics.CompileShader([]byte(screenShaderSrc))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("ui: compiling the screen shader failed: %v", err))
|
||||
}
|
||||
screenShader = NewShader(ir)
|
||||
}
|
||||
}
|
||||
|
||||
type Game interface {
|
||||
NewOffscreenImage(width, height int) *Image
|
||||
NewScreenImage(width, height int) *Image
|
||||
Layout(outsideWidth, outsideHeight int) (int, int)
|
||||
Update() error
|
||||
Draw()
|
||||
DrawOffscreen()
|
||||
DrawScreen(scale, offsetX, offsteY float64)
|
||||
}
|
||||
|
||||
type context struct {
|
||||
@ -211,51 +164,14 @@ func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics) {
|
||||
if theGlobalState.isScreenClearedEveryFrame() {
|
||||
c.offscreen.clear()
|
||||
}
|
||||
c.game.Draw()
|
||||
c.game.DrawOffscreen()
|
||||
|
||||
if graphicsDriver.NeedsClearingScreen() {
|
||||
// This clear is needed for fullscreen mode or some mobile platforms (#622).
|
||||
c.screen.clear()
|
||||
}
|
||||
|
||||
screenScale, offsetX, offsetY := c.screenScaleAndOffsets()
|
||||
|
||||
var shader *Shader
|
||||
switch {
|
||||
case !theGlobalState.isScreenFilterEnabled():
|
||||
shader = NearestFilterShader
|
||||
case math.Floor(screenScale) == screenScale:
|
||||
shader = NearestFilterShader
|
||||
case screenScale > 1:
|
||||
shader = screenShader
|
||||
default:
|
||||
// screenShader works with >=1 scale, but does not well with <1 scale.
|
||||
// Use regular FilterLinear instead so far (#669).
|
||||
shader = LinearFilterShader
|
||||
}
|
||||
|
||||
dstRegion := graphicsdriver.Region{
|
||||
X: 0,
|
||||
Y: 0,
|
||||
Width: float32(c.screen.width),
|
||||
Height: float32(c.screen.height),
|
||||
}
|
||||
srcRegion := graphicsdriver.Region{
|
||||
X: 0,
|
||||
Y: 0,
|
||||
Width: float32(c.offscreen.width),
|
||||
Height: float32(c.offscreen.height),
|
||||
}
|
||||
|
||||
vs := graphics.QuadVertices(
|
||||
0, 0, float32(c.offscreen.width), float32(c.offscreen.height),
|
||||
float32(screenScale), 0, 0, float32(screenScale), float32(offsetX), float32(offsetY),
|
||||
1, 1, 1, 1)
|
||||
is := graphics.QuadIndices()
|
||||
|
||||
srcs := [graphics.ShaderImageCount]*Image{c.offscreen}
|
||||
|
||||
c.screen.DrawTriangles(srcs, vs, is, graphicsdriver.CompositeModeCopy, dstRegion, srcRegion, [graphics.ShaderImageCount - 1][2]float32{}, shader, nil, false, true)
|
||||
c.game.DrawScreen(c.screenScaleAndOffsets())
|
||||
}
|
||||
|
||||
func (c *context) layoutGame(outsideWidth, outsideHeight float64, deviceScaleFactor float64) (int, int) {
|
||||
@ -287,7 +203,7 @@ func (c *context) layoutGame(outsideWidth, outsideHeight float64, deviceScaleFac
|
||||
}
|
||||
}
|
||||
if c.screen == nil {
|
||||
c.screen = NewImage(sw, sh, atlas.ImageTypeScreen)
|
||||
c.screen = c.game.NewScreenImage(sw, sh)
|
||||
}
|
||||
|
||||
if c.offscreen != nil {
|
||||
@ -333,7 +249,6 @@ func (c *context) screenScaleAndOffsets() (float64, float64, float64) {
|
||||
|
||||
var theGlobalState = globalState{
|
||||
isScreenClearedEveryFrame_: 1,
|
||||
screenFilterEnabled_: 1,
|
||||
}
|
||||
|
||||
// globalState represents a global state in this package.
|
||||
@ -344,7 +259,6 @@ type globalState struct {
|
||||
|
||||
fpsMode_ int32
|
||||
isScreenClearedEveryFrame_ int32
|
||||
screenFilterEnabled_ int32
|
||||
graphicsLibrary_ int32
|
||||
}
|
||||
|
||||
@ -382,18 +296,6 @@ func (g *globalState) setScreenClearedEveryFrame(cleared bool) {
|
||||
atomic.StoreInt32(&g.isScreenClearedEveryFrame_, v)
|
||||
}
|
||||
|
||||
func (g *globalState) isScreenFilterEnabled() bool {
|
||||
return atomic.LoadInt32(&g.screenFilterEnabled_) != 0
|
||||
}
|
||||
|
||||
func (g *globalState) setScreenFilterEnabled(enabled bool) {
|
||||
v := int32(0)
|
||||
if enabled {
|
||||
v = 1
|
||||
}
|
||||
atomic.StoreInt32(&g.screenFilterEnabled_, v)
|
||||
}
|
||||
|
||||
func (g *globalState) setGraphicsLibrary(library GraphicsLibrary) {
|
||||
atomic.StoreInt32(&g.graphicsLibrary_, int32(library))
|
||||
}
|
||||
@ -419,14 +321,6 @@ func SetScreenClearedEveryFrame(cleared bool) {
|
||||
theGlobalState.setScreenClearedEveryFrame(cleared)
|
||||
}
|
||||
|
||||
func IsScreenFilterEnabled() bool {
|
||||
return theGlobalState.isScreenFilterEnabled()
|
||||
}
|
||||
|
||||
func SetScreenFilterEnabled(enabled bool) {
|
||||
theGlobalState.setScreenFilterEnabled(enabled)
|
||||
}
|
||||
|
||||
func GetGraphicsLibrary() GraphicsLibrary {
|
||||
return theGlobalState.graphicsLibrary()
|
||||
}
|
||||
|
4
run.go
4
run.go
@ -133,14 +133,14 @@ func IsScreenClearedEveryFrame() bool {
|
||||
//
|
||||
// SetScreenFilterEnabled is concurrent-safe, but takes effect only at the next Draw call.
|
||||
func SetScreenFilterEnabled(enabled bool) {
|
||||
ui.SetScreenFilterEnabled(enabled)
|
||||
setScreenFilterEnabled(enabled)
|
||||
}
|
||||
|
||||
// IsScreenFilterEnabled returns true if Ebitengine's "screen" filter is enabled.
|
||||
//
|
||||
// IsScreenFilterEnabled is concurrent-safe.
|
||||
func IsScreenFilterEnabled() bool {
|
||||
return ui.IsScreenFilterEnabled()
|
||||
return isScreenFilterEnabled()
|
||||
}
|
||||
|
||||
type imageDumperGame struct {
|
||||
|
Loading…
Reference in New Issue
Block a user