diff --git a/doc.go b/doc.go index f64b7fd4c..a76e13744 100644 --- a/doc.go +++ b/doc.go @@ -72,6 +72,8 @@ // // `ebitengl` forces to use OpenGL in any environments. // +// `ebitenwebgl1` forces to use WebGL 1 on browsers. +// // `ebitensinglethread` disables Ebiten's thread safety to unlock maximum performance. If you use this you will have // to manage threads yourself. Functions like IsKeyPressed will no longer be concurrent-safe with this build tag. // They must be called from the main thread or the same goroutine as the given game's callback functions like Update diff --git a/internal/graphicsdriver/opengl/context_js.go b/internal/graphicsdriver/opengl/context_js.go index 0cb553988..613ab8239 100644 --- a/internal/graphicsdriver/opengl/context_js.go +++ b/internal/graphicsdriver/opengl/context_js.go @@ -94,7 +94,7 @@ const ( ) var ( - isWebGL2Available = js.Global().Get("WebGL2RenderingContext").Truthy() + isWebGL2Available = !forceWebGL1 && js.Global().Get("WebGL2RenderingContext").Truthy() ) type contextImpl struct { @@ -148,6 +148,10 @@ func (c *context) reset() error { c.blendFunc(driver.CompositeModeSourceOver) f := gl.Call("getParameter", gles.FRAMEBUFFER_BINDING) c.screenFramebuffer = framebufferNative(f) + + if !isWebGL2Available { + gl.Call("getExtension", "OES_standard_derivatives") + } return nil } diff --git a/internal/graphicsdriver/opengl/context_notwebgl1.go b/internal/graphicsdriver/opengl/context_notwebgl1.go new file mode 100644 index 000000000..6c586bd57 --- /dev/null +++ b/internal/graphicsdriver/opengl/context_notwebgl1.go @@ -0,0 +1,19 @@ +// Copyright 2020 The Ebiten 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. + +// +build !ebitenwebgl1 + +package opengl + +const forceWebGL1 = false diff --git a/internal/graphicsdriver/opengl/context_webgl1.go b/internal/graphicsdriver/opengl/context_webgl1.go new file mode 100644 index 000000000..5a379b97d --- /dev/null +++ b/internal/graphicsdriver/opengl/context_webgl1.go @@ -0,0 +1,19 @@ +// Copyright 2020 The Ebiten 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. + +// +build ebitenwebgl1 + +package opengl + +const forceWebGL1 = true diff --git a/internal/graphicsdriver/opengl/shader.go b/internal/graphicsdriver/opengl/shader.go index d5d8d7d0e..4e938bad2 100644 --- a/internal/graphicsdriver/opengl/shader.go +++ b/internal/graphicsdriver/opengl/shader.go @@ -52,7 +52,7 @@ func (s *Shader) Dispose() { } func (s *Shader) compile() error { - vssrc, fssrc := glsl.Compile(s.ir) + vssrc, fssrc := glsl.Compile(s.ir, glslVersion()) vs, err := s.graphics.context.newShader(vertexShader, vssrc) if err != nil { diff --git a/internal/graphicsdriver/opengl/shader_js.go b/internal/graphicsdriver/opengl/shader_js.go new file mode 100644 index 000000000..d63c5352c --- /dev/null +++ b/internal/graphicsdriver/opengl/shader_js.go @@ -0,0 +1,26 @@ +// Copyright 2020 The Ebiten 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 opengl + +import ( + "github.com/hajimehoshi/ebiten/v2/internal/shaderir/glsl" +) + +func glslVersion() glsl.GLSLVersion { + if isWebGL2Available { + return glsl.GLSLVersionES300 + } + return glsl.GLSLVersionWebGL1 +} diff --git a/internal/graphicsdriver/opengl/shader_notjs.go b/internal/graphicsdriver/opengl/shader_notjs.go new file mode 100644 index 000000000..198ddd1a5 --- /dev/null +++ b/internal/graphicsdriver/opengl/shader_notjs.go @@ -0,0 +1,25 @@ +// Copyright 2020 The Ebiten 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. + +// +build !js + +package opengl + +import ( + "github.com/hajimehoshi/ebiten/v2/internal/shaderir/glsl" +) + +func glslVersion() glsl.GLSLVersion { + return glsl.GLSLVersionDefault +} diff --git a/internal/shader/shader_test.go b/internal/shader/shader_test.go index fc6358b9f..df3dfcb6c 100644 --- a/internal/shader/shader_test.go +++ b/internal/shader/shader_test.go @@ -30,8 +30,9 @@ import ( ) func glslNormalize(str string) string { - if strings.HasPrefix(str, glsl.FragmentPrelude) { - str = str[len(glsl.FragmentPrelude):] + p := glsl.FragmentPrelude(glsl.GLSLVersionDefault) + if strings.HasPrefix(str, p) { + str = str[len(p):] } return strings.TrimSpace(str) } @@ -156,7 +157,7 @@ func TestCompile(t *testing.T) { } // GLSL - vs, fs := glsl.Compile(s) + vs, fs := glsl.Compile(s, glsl.GLSLVersionDefault) if got, want := glslNormalize(vs), glslNormalize(string(tc.VS)); got != want { compare(t, "GLSL Vertex", got, want) } diff --git a/internal/shaderir/glsl/glsl.go b/internal/shaderir/glsl/glsl.go index 6d155ccb0..8cf1b95cb 100644 --- a/internal/shaderir/glsl/glsl.go +++ b/internal/shaderir/glsl/glsl.go @@ -24,15 +24,40 @@ import ( "github.com/hajimehoshi/ebiten/v2/internal/shaderir" ) -const FragmentPrelude = `#if defined(GL_ES) +type GLSLVersion int + +const ( + GLSLVersionDefault GLSLVersion = iota + GLSLVersionWebGL1 + GLSLVersionES300 +) + +func VertexPrelude(version GLSLVersion) string { + if version == GLSLVersionES300 { + return `#version 300 es` + } + return "" +} + +func FragmentPrelude(version GLSLVersion) string { + var prefix string + switch version { + case GLSLVersionWebGL1: + prefix = `#extension GL_OES_standard_derivatives : enable` + "\n\n" + case GLSLVersionES300: + prefix = `#version 300 es` + "\n\n" + } + return prefix + `#if defined(GL_ES) precision highp float; #else #define lowp #define mediump #define highp #endif` +} type compileContext struct { + version GLSLVersion structNames map[string]string structTypes []shaderir.Type } @@ -51,14 +76,16 @@ func (c *compileContext) structName(p *shaderir.Program, t *shaderir.Type) strin return n } -func Compile(p *shaderir.Program) (vertexShader, fragmentShader string) { +func Compile(p *shaderir.Program, version GLSLVersion) (vertexShader, fragmentShader string) { c := &compileContext{ + version: version, structNames: map[string]string{}, } // Vertex func var vslines []string { + vslines = append(vslines, strings.Split(VertexPrelude(version), "\n")...) vslines = append(vslines, "{{.Structs}}") if len(p.Uniforms) > 0 || p.TextureNum > 0 || len(p.Attributes) > 0 || len(p.Varyings) > 0 { vslines = append(vslines, "") @@ -69,10 +96,18 @@ func Compile(p *shaderir.Program) (vertexShader, fragmentShader string) { vslines = append(vslines, fmt.Sprintf("uniform sampler2D T%d;", i)) } for i, t := range p.Attributes { - vslines = append(vslines, fmt.Sprintf("attribute %s;", c.glslVarDecl(p, &t, fmt.Sprintf("A%d", i)))) + keyword := "attribute" + if version == GLSLVersionES300 { + keyword = "in" + } + vslines = append(vslines, fmt.Sprintf("%s %s;", keyword, c.glslVarDecl(p, &t, fmt.Sprintf("A%d", i)))) } for i, t := range p.Varyings { - vslines = append(vslines, fmt.Sprintf("varying %s;", c.glslVarDecl(p, &t, fmt.Sprintf("V%d", i)))) + keyword := "varying" + if version == GLSLVersionES300 { + keyword = "out" + } + vslines = append(vslines, fmt.Sprintf("%s %s;", keyword, c.glslVarDecl(p, &t, fmt.Sprintf("V%d", i)))) } } if len(p.Funcs) > 0 { @@ -99,7 +134,7 @@ func Compile(p *shaderir.Program) (vertexShader, fragmentShader string) { // Fragment func var fslines []string { - fslines = append(fslines, strings.Split(FragmentPrelude, "\n")...) + fslines = append(fslines, strings.Split(FragmentPrelude(version), "\n")...) fslines = append(fslines, "", "{{.Structs}}") if len(p.Uniforms) > 0 || p.TextureNum > 0 || len(p.Varyings) > 0 { fslines = append(fslines, "") @@ -110,9 +145,17 @@ func Compile(p *shaderir.Program) (vertexShader, fragmentShader string) { fslines = append(fslines, fmt.Sprintf("uniform sampler2D T%d;", i)) } for i, t := range p.Varyings { - fslines = append(fslines, fmt.Sprintf("varying %s;", c.glslVarDecl(p, &t, fmt.Sprintf("V%d", i)))) + keyword := "varying" + if version == GLSLVersionES300 { + keyword = "in" + } + fslines = append(fslines, fmt.Sprintf("%s %s;", keyword, c.glslVarDecl(p, &t, fmt.Sprintf("V%d", i)))) } } + if version == GLSLVersionES300 { + fslines = append(fslines, "out vec4 fragColor;") + } + if len(p.Funcs) > 0 { fslines = append(fslines, "") for _, f := range p.Funcs { @@ -273,7 +316,7 @@ func constantToNumberLiteral(t shaderir.ConstType, v constant.Value) string { return fmt.Sprintf("?(unexpected literal: %s)", v) } -func localVariableName(p *shaderir.Program, topBlock, block *shaderir.Block, idx int) string { +func (c *compileContext) localVariableName(p *shaderir.Program, topBlock, block *shaderir.Block, idx int) string { switch topBlock { case p.VertexFunc.Block: na := len(p.Attributes) @@ -296,6 +339,9 @@ func localVariableName(p *shaderir.Program, topBlock, block *shaderir.Block, idx case idx < nv+1: return fmt.Sprintf("V%d", idx-1) case idx == nv+1: + if c.version == GLSLVersionES300 { + return "fragColor" + } return "gl_FragColor" default: return fmt.Sprintf("l%d", idx-(nv+2)) @@ -307,7 +353,7 @@ func localVariableName(p *shaderir.Program, topBlock, block *shaderir.Block, idx func (c *compileContext) initVariable(p *shaderir.Program, topBlock, block *shaderir.Block, index int, decl bool, level int) []string { idt := strings.Repeat("\t", level+1) - name := localVariableName(p, topBlock, block, index) + name := c.localVariableName(p, topBlock, block, index) t := p.LocalVariableType(topBlock, block, index) var lines []string @@ -352,11 +398,11 @@ func (c *compileContext) glslBlock(p *shaderir.Program, topBlock, block *shaderi case shaderir.TextureVariable: return fmt.Sprintf("T%d", e.Index) case shaderir.LocalVariable: - return localVariableName(p, topBlock, block, e.Index) + return c.localVariableName(p, topBlock, block, e.Index) case shaderir.StructMember: return fmt.Sprintf("M%d", e.Index) case shaderir.BuiltinFuncExpr: - return builtinFuncString(e.BuiltinFunc) + return c.builtinFuncString(e.BuiltinFunc) case shaderir.SwizzlingExpr: if !shaderir.IsValidSwizzling(e.Swizzling) { return fmt.Sprintf("?(unexpected swizzling: %s)", e.Swizzling) @@ -433,7 +479,7 @@ func (c *compileContext) glslBlock(p *shaderir.Program, topBlock, block *shaderi ct = shaderir.ConstTypeFloat } - v := localVariableName(p, topBlock, block, s.ForVarIndex) + v := c.localVariableName(p, topBlock, block, s.ForVarIndex) var delta string switch val, _ := constant.Float64Val(s.ForDelta); val { case 0: diff --git a/internal/shaderir/glsl/type.go b/internal/shaderir/glsl/type.go index f8b3a3ca5..f8e35f540 100644 --- a/internal/shaderir/glsl/type.go +++ b/internal/shaderir/glsl/type.go @@ -63,7 +63,7 @@ func basicTypeString(t shaderir.BasicType) string { } } -func builtinFuncString(f shaderir.BuiltinFunc) string { +func (c *compileContext) builtinFuncString(f shaderir.BuiltinFunc) string { switch f { case shaderir.Atan2: return "atan" @@ -71,6 +71,11 @@ func builtinFuncString(f shaderir.BuiltinFunc) string { return "dFdx" case shaderir.Dfdy: return "dFdy" + case shaderir.Texture2DF: + if c.version == GLSLVersionES300 { + return "texture" + } + return "texture2D" default: return string(f) } diff --git a/shader_test.go b/shader_test.go index d26a8a377..525260643 100644 --- a/shader_test.go +++ b/shader_test.go @@ -924,3 +924,56 @@ func Fragment(position vec4, texCoord vec2, color vec4) vec4 { testPixels("DrawTrianglesShader", dst) }) } + +// Issue #1404 +func TestShaderDerivatives(t *testing.T) { + const w, h = 16, 16 + + s, err := NewShader([]byte(`package main + +func Fragment(position vec4, texCoord vec2, color vec4) vec4 { + p := imageSrc0At(texCoord) + return vec4(abs(dfdx(p.r)), abs(dfdy(p.g)), 0, 1) +} +`)) + if err != nil { + t.Fatal(err) + } + + dst := NewImage(w, h) + src := NewImage(w, h) + pix := make([]byte, 4*w*h) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + if i < w/2 { + pix[4*(j*w+i)] = 0xff + } + if j < h/2 { + pix[4*(j*w+i)+1] = 0xff + } + pix[4*(j*w+i)+3] = 0xff + } + } + src.ReplacePixels(pix) + + op := &DrawRectShaderOptions{} + op.Images[0] = src + dst.DrawRectShader(w, h, s, op) + + // The results of the edges might be unreliable. Skip the edges. + for j := 1; j < h-1; j++ { + for i := 1; i < w-1; i++ { + got := dst.At(i, j).(color.RGBA) + want := color.RGBA{0, 0, 0, 0xff} + if i == w/2-1 || i == w/2 { + want.R = 0xff + } + if j == h/2-1 || j == h/2 { + want.G = 0xff + } + if got != want { + t.Errorf("dst.At(%d, %d): got: %v, want: %v", i, j, got, want) + } + } + } +}