ebiten: add FinalScreenDrawer

FinalScreenDrawer is an interface for a custom screen rendering. If a
game implements FinalScreenDrawer and is passed to RunGame, its
DrawFinalScreen is called after Draw.

Also this adds `-crt` option to examples/flappy.

Closes #2046
This commit is contained in:
Hajime Hoshi 2022-10-14 02:22:40 +09:00
parent 0803342d01
commit 30cc36b1ba
5 changed files with 126 additions and 2 deletions

42
examples/flappy/crt.go Normal file
View File

@ -0,0 +1,42 @@
// 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.
//go:build ignore
// +build ignore
// Reference: a public domain CRT effect
// https://github.com/libretro/glsl-shaders/blob/master/crt/shaders/crt-lottes.glsl
package main
func Warp(pos vec2) vec2 {
const (
warpX = 0.031
warpY = 0.041
)
pos = pos*2 - 1
pos *= vec2(1+(pos.y*pos.y)*warpX, 1+(pos.x*pos.x)*warpY)
return pos/2 + 0.5
}
func Fragment(position vec4, texCoord vec2, color vec4) vec4 {
// Adjust the texture position to [0, 1].
pos := texCoord
origin, size := imageSrcRegionOnTexture()
pos -= origin
pos /= size
pos = Warp(pos)
return imageSrc0At(pos*size + origin)
}

View File

@ -19,6 +19,8 @@ package main
import ( import (
"bytes" "bytes"
_ "embed"
"flag"
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
@ -43,6 +45,11 @@ import (
"github.com/hajimehoshi/ebiten/v2/text" "github.com/hajimehoshi/ebiten/v2/text"
) )
var flagCRT = flag.Bool("crt", false, "enable the CRT effect")
//go:embed crt.go
var crtGo []byte
func init() { func init() {
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
} }
@ -159,9 +166,12 @@ type Game struct {
hitPlayer *audio.Player hitPlayer *audio.Player
} }
func NewGame() *Game { func NewGame(crt bool) ebiten.Game {
g := &Game{} g := &Game{}
g.init() g.init()
if crt {
return &GameWithCRTEffect{Game: g}
}
return g return g
} }
@ -439,10 +449,35 @@ func (g *Game) drawGopher(screen *ebiten.Image) {
screen.DrawImage(gopherImage, op) screen.DrawImage(gopherImage, op)
} }
type GameWithCRTEffect struct {
ebiten.Game
crtShader *ebiten.Shader
}
func (g *GameWithCRTEffect) DrawFinalScreen(screen ebiten.FinalScreen, offscreen *ebiten.Image) {
if g.crtShader == nil {
s, err := ebiten.NewShader(crtGo)
if err != nil {
panic(fmt.Sprintf("flappy: filed to compiled the CRT shader: %v", err))
}
g.crtShader = s
}
ow, oh := offscreen.Size()
sw, sh := screen.Size()
op := &ebiten.DrawRectShaderOptions{}
op.Images[0] = offscreen
op.GeoM.Scale(float64(sw)/float64(ow), float64(sh)/float64(oh))
screen.DrawRectShader(ow, oh, g.crtShader, op)
}
func main() { func main() {
flag.Parse()
ebiten.SetWindowSize(screenWidth, screenHeight) ebiten.SetWindowSize(screenWidth, screenHeight)
ebiten.SetWindowTitle("Flappy Gopher (Ebitengine Demo)") ebiten.SetWindowTitle("Flappy Gopher (Ebitengine Demo)")
if err := ebiten.RunGame(NewGame()); err != nil { if err := ebiten.RunGame(NewGame(*flagCRT)); err != nil {
panic(err) panic(err)
} }
} }

View File

@ -148,6 +148,11 @@ func (g *gameForUI) DrawOffscreen() error {
} }
func (g *gameForUI) DrawScreen() { func (g *gameForUI) DrawScreen() {
if d, ok := g.game.(FinalScreenDrawer); ok {
d.DrawFinalScreen(g.screen, g.offscreen)
return
}
scale, offsetX, offsetY := g.ScreenScaleAndOffsets() scale, offsetX, offsetY := g.ScreenScaleAndOffsets()
switch { switch {
case !isScreenFilterEnabled(), math.Floor(scale) == scale: case !isScreenFilterEnabled(), math.Floor(scale) == scale:

View File

@ -1090,3 +1090,7 @@ func colorMToScale(colorm affine.ColorM) (newColorM affine.ColorM, r, g, b, a fl
return affine.ColorMIdentity{}, r * a, g * a, b * a, a return affine.ColorMIdentity{}, r * a, g * a, b * a, a
} }
// private implements FinalScreen.
func (*Image) private() {
}

38
run.go
View File

@ -16,6 +16,8 @@ package ebiten
import ( import (
"errors" "errors"
"image"
"image/color"
"sync/atomic" "sync/atomic"
"github.com/hajimehoshi/ebiten/v2/internal/clock" "github.com/hajimehoshi/ebiten/v2/internal/clock"
@ -80,6 +82,34 @@ type Game interface {
Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int)
} }
// FinalScreen represents the final screen image.
// FinalScreen implements a part of Image functions.
type FinalScreen interface {
Bounds() image.Rectangle
Size() (int, int)
DrawImage(img *Image, options *DrawImageOptions)
DrawTriangles(vertices []Vertex, indices []uint16, img *Image, options *DrawTrianglesOptions)
DrawRectShader(width, height int, shader *Shader, options *DrawRectShaderOptions)
DrawTrianglesShader(vertices []Vertex, indices []uint16, shader *Shader, options *DrawTrianglesShaderOptions)
Clear()
Fill(clr color.Color)
// private prevents other packages from implementing this interface.
// A new function might be added to this interface in the future
// even if the Ebitengine major version is not updated.
private()
}
// FinalScreenDrawer is an interface for a custom function to render the final screen.
// For an actual usage, see examples/flappy.
type FinalScreenDrawer interface {
// DrawFinalScreen draws the final screen.
// If a game implementing FinalScreenDrawer is passed to RunGame, DrawFinalScreen is called after Draw.
// screen is the final screen. offscreen is the offscreen modified at Draw.
DrawFinalScreen(screen FinalScreen, offscreen *Image)
}
// DefaultTPS represents a default ticks per second, that represents how many times game updating happens in a second. // DefaultTPS represents a default ticks per second, that represents how many times game updating happens in a second.
const DefaultTPS = clock.DefaultTPS const DefaultTPS = clock.DefaultTPS
@ -132,6 +162,8 @@ func IsScreenClearedEveryFrame() bool {
// The default state is true. // The default state is true.
// //
// 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.
//
// Deprecated: as of v2.5. Use FinalScreenDrawer instead.
func SetScreenFilterEnabled(enabled bool) { func SetScreenFilterEnabled(enabled bool) {
setScreenFilterEnabled(enabled) setScreenFilterEnabled(enabled)
} }
@ -139,6 +171,8 @@ func SetScreenFilterEnabled(enabled bool) {
// 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.
//
// Deprecated: as of v2.5. Use FinalScreenDrawer instead.
func IsScreenFilterEnabled() bool { func IsScreenFilterEnabled() bool {
return isScreenFilterEnabled() return isScreenFilterEnabled()
} }
@ -151,6 +185,10 @@ var Termination = ui.RegularTermination
// game's Draw function is called every frame to draw the screen. // game's Draw function is called every frame to draw the screen.
// game's Layout function is called when necessary, and you can specify the logical screen size by the function. // game's Layout function is called when necessary, and you can specify the logical screen size by the function.
// //
// If game implements FinalScreenDrawer, its DrawFinalScreen is called after Draw.
// The argument screen represents the final screen. The argument offscreen is an offscreen modified at Draw.
// If game does not implement FinalScreenDrawer, the dafault rendering for the final screen is used.
//
// game's functions are called on the same goroutine. // game's functions are called on the same goroutine.
// //
// On browsers, it is strongly recommended to use iframe if you embed an Ebitengine application in your website. // On browsers, it is strongly recommended to use iframe if you embed an Ebitengine application in your website.