diff --git a/examples/flappy/crt.go b/examples/flappy/crt.go new file mode 100644 index 000000000..4ba45aebd --- /dev/null +++ b/examples/flappy/crt.go @@ -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) +} diff --git a/examples/flappy/main.go b/examples/flappy/main.go index ace311b24..db8c3d310 100644 --- a/examples/flappy/main.go +++ b/examples/flappy/main.go @@ -19,6 +19,8 @@ package main import ( "bytes" + _ "embed" + "flag" "fmt" "image" "image/color" @@ -43,6 +45,11 @@ import ( "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() { rand.Seed(time.Now().UnixNano()) } @@ -159,9 +166,12 @@ type Game struct { hitPlayer *audio.Player } -func NewGame() *Game { +func NewGame(crt bool) ebiten.Game { g := &Game{} g.init() + if crt { + return &GameWithCRTEffect{Game: g} + } return g } @@ -439,10 +449,35 @@ func (g *Game) drawGopher(screen *ebiten.Image) { 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() { + flag.Parse() ebiten.SetWindowSize(screenWidth, screenHeight) ebiten.SetWindowTitle("Flappy Gopher (Ebitengine Demo)") - if err := ebiten.RunGame(NewGame()); err != nil { + if err := ebiten.RunGame(NewGame(*flagCRT)); err != nil { panic(err) } } diff --git a/gameforui.go b/gameforui.go index b61cc62ee..da4bfb2d8 100644 --- a/gameforui.go +++ b/gameforui.go @@ -148,6 +148,11 @@ func (g *gameForUI) DrawOffscreen() error { } func (g *gameForUI) DrawScreen() { + if d, ok := g.game.(FinalScreenDrawer); ok { + d.DrawFinalScreen(g.screen, g.offscreen) + return + } + scale, offsetX, offsetY := g.ScreenScaleAndOffsets() switch { case !isScreenFilterEnabled(), math.Floor(scale) == scale: diff --git a/image.go b/image.go index 723875410..925a37b7a 100644 --- a/image.go +++ b/image.go @@ -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 } + +// private implements FinalScreen. +func (*Image) private() { +} diff --git a/run.go b/run.go index c6420391c..9c5046a34 100644 --- a/run.go +++ b/run.go @@ -16,6 +16,8 @@ package ebiten import ( "errors" + "image" + "image/color" "sync/atomic" "github.com/hajimehoshi/ebiten/v2/internal/clock" @@ -80,6 +82,34 @@ type Game interface { 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. const DefaultTPS = clock.DefaultTPS @@ -132,6 +162,8 @@ func IsScreenClearedEveryFrame() bool { // The default state is true. // // 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) { setScreenFilterEnabled(enabled) } @@ -139,6 +171,8 @@ func SetScreenFilterEnabled(enabled bool) { // IsScreenFilterEnabled returns true if Ebitengine's "screen" filter is enabled. // // IsScreenFilterEnabled is concurrent-safe. +// +// Deprecated: as of v2.5. Use FinalScreenDrawer instead. func IsScreenFilterEnabled() bool { return isScreenFilterEnabled() } @@ -151,6 +185,10 @@ var Termination = ui.RegularTermination // 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. // +// 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. // // On browsers, it is strongly recommended to use iframe if you embed an Ebitengine application in your website.