mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-25 03:08: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
|
package ebiten
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
"math"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
|
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
|
||||||
"github.com/hajimehoshi/ebiten/v2/internal/ui"
|
"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 {
|
type gameForUI struct {
|
||||||
game Game
|
game Game
|
||||||
offscreen *Image
|
offscreen *Image
|
||||||
|
screen *Image
|
||||||
|
screenShader *Shader
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGameForUI(game Game) *gameForUI {
|
func newGameForUI(game Game) *gameForUI {
|
||||||
return &gameForUI{
|
g := &gameForUI{
|
||||||
game: game,
|
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 {
|
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
|
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) {
|
func (c *gameForUI) Layout(outsideWidth, outsideHeight int) (int, int) {
|
||||||
return c.game.Layout(outsideWidth, outsideHeight)
|
return c.game.Layout(outsideWidth, outsideHeight)
|
||||||
}
|
}
|
||||||
@ -59,6 +132,29 @@ func (c *gameForUI) Update() error {
|
|||||||
return c.game.Update()
|
return c.game.Update()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *gameForUI) Draw() {
|
func (c *gameForUI) DrawOffscreen() {
|
||||||
c.game.Draw(c.offscreen)
|
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
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"math"
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
|
|
||||||
"github.com/hajimehoshi/ebiten/v2/internal/buffered"
|
"github.com/hajimehoshi/ebiten/v2/internal/buffered"
|
||||||
"github.com/hajimehoshi/ebiten/v2/internal/clock"
|
"github.com/hajimehoshi/ebiten/v2/internal/clock"
|
||||||
"github.com/hajimehoshi/ebiten/v2/internal/debug"
|
"github.com/hajimehoshi/ebiten/v2/internal/debug"
|
||||||
@ -30,63 +28,18 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten/v2/internal/mipmap"
|
"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 (
|
var (
|
||||||
screenShader *Shader
|
|
||||||
NearestFilterShader = &Shader{shader: mipmap.NearestFilterShader}
|
NearestFilterShader = &Shader{shader: mipmap.NearestFilterShader}
|
||||||
LinearFilterShader = &Shader{shader: mipmap.LinearFilterShader}
|
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 {
|
type Game interface {
|
||||||
NewOffscreenImage(width, height int) *Image
|
NewOffscreenImage(width, height int) *Image
|
||||||
|
NewScreenImage(width, height int) *Image
|
||||||
Layout(outsideWidth, outsideHeight int) (int, int)
|
Layout(outsideWidth, outsideHeight int) (int, int)
|
||||||
Update() error
|
Update() error
|
||||||
Draw()
|
DrawOffscreen()
|
||||||
|
DrawScreen(scale, offsetX, offsteY float64)
|
||||||
}
|
}
|
||||||
|
|
||||||
type context struct {
|
type context struct {
|
||||||
@ -211,51 +164,14 @@ func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics) {
|
|||||||
if theGlobalState.isScreenClearedEveryFrame() {
|
if theGlobalState.isScreenClearedEveryFrame() {
|
||||||
c.offscreen.clear()
|
c.offscreen.clear()
|
||||||
}
|
}
|
||||||
c.game.Draw()
|
c.game.DrawOffscreen()
|
||||||
|
|
||||||
if graphicsDriver.NeedsClearingScreen() {
|
if graphicsDriver.NeedsClearingScreen() {
|
||||||
// This clear is needed for fullscreen mode or some mobile platforms (#622).
|
// This clear is needed for fullscreen mode or some mobile platforms (#622).
|
||||||
c.screen.clear()
|
c.screen.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
screenScale, offsetX, offsetY := c.screenScaleAndOffsets()
|
c.game.DrawScreen(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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *context) layoutGame(outsideWidth, outsideHeight float64, deviceScaleFactor float64) (int, int) {
|
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 {
|
if c.screen == nil {
|
||||||
c.screen = NewImage(sw, sh, atlas.ImageTypeScreen)
|
c.screen = c.game.NewScreenImage(sw, sh)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.offscreen != nil {
|
if c.offscreen != nil {
|
||||||
@ -333,7 +249,6 @@ func (c *context) screenScaleAndOffsets() (float64, float64, float64) {
|
|||||||
|
|
||||||
var theGlobalState = globalState{
|
var theGlobalState = globalState{
|
||||||
isScreenClearedEveryFrame_: 1,
|
isScreenClearedEveryFrame_: 1,
|
||||||
screenFilterEnabled_: 1,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// globalState represents a global state in this package.
|
// globalState represents a global state in this package.
|
||||||
@ -344,7 +259,6 @@ type globalState struct {
|
|||||||
|
|
||||||
fpsMode_ int32
|
fpsMode_ int32
|
||||||
isScreenClearedEveryFrame_ int32
|
isScreenClearedEveryFrame_ int32
|
||||||
screenFilterEnabled_ int32
|
|
||||||
graphicsLibrary_ int32
|
graphicsLibrary_ int32
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -382,18 +296,6 @@ func (g *globalState) setScreenClearedEveryFrame(cleared bool) {
|
|||||||
atomic.StoreInt32(&g.isScreenClearedEveryFrame_, v)
|
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) {
|
func (g *globalState) setGraphicsLibrary(library GraphicsLibrary) {
|
||||||
atomic.StoreInt32(&g.graphicsLibrary_, int32(library))
|
atomic.StoreInt32(&g.graphicsLibrary_, int32(library))
|
||||||
}
|
}
|
||||||
@ -419,14 +321,6 @@ func SetScreenClearedEveryFrame(cleared bool) {
|
|||||||
theGlobalState.setScreenClearedEveryFrame(cleared)
|
theGlobalState.setScreenClearedEveryFrame(cleared)
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsScreenFilterEnabled() bool {
|
|
||||||
return theGlobalState.isScreenFilterEnabled()
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetScreenFilterEnabled(enabled bool) {
|
|
||||||
theGlobalState.setScreenFilterEnabled(enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetGraphicsLibrary() GraphicsLibrary {
|
func GetGraphicsLibrary() GraphicsLibrary {
|
||||||
return theGlobalState.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.
|
// SetScreenFilterEnabled is concurrent-safe, but takes effect only at the next Draw call.
|
||||||
func SetScreenFilterEnabled(enabled bool) {
|
func SetScreenFilterEnabled(enabled bool) {
|
||||||
ui.SetScreenFilterEnabled(enabled)
|
setScreenFilterEnabled(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsScreenFilterEnabled returns true if Ebitengine's "screen" filter is enabled.
|
// IsScreenFilterEnabled returns true if Ebitengine's "screen" filter is enabled.
|
||||||
//
|
//
|
||||||
// IsScreenFilterEnabled is concurrent-safe.
|
// IsScreenFilterEnabled is concurrent-safe.
|
||||||
func IsScreenFilterEnabled() bool {
|
func IsScreenFilterEnabled() bool {
|
||||||
return ui.IsScreenFilterEnabled()
|
return isScreenFilterEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
type imageDumperGame struct {
|
type imageDumperGame struct {
|
||||||
|
Loading…
Reference in New Issue
Block a user