From b1d7a5f595c7b6e531ab238332153c38552f59b3 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Mon, 26 Oct 2020 10:33:11 +0900 Subject: [PATCH] shaderir/glsl: Enable dFdx for WebGL With WebGL1, an extension is required for dFdx. On the other hand, with WebGL2, GLSL ES 300 is required and the extension is forbidden. This change fixes shaderir/glsl to switch the output depends on the WebGL version. This change also adds a new build tag 'ebitenwebgl1' forcing WebGL 1. Updates #1404 --- doc.go | 2 + internal/graphicsdriver/opengl/context_js.go | 6 +- .../opengl/context_notwebgl1.go | 19 ++++++ .../graphicsdriver/opengl/context_webgl1.go | 19 ++++++ internal/graphicsdriver/opengl/shader.go | 2 +- internal/graphicsdriver/opengl/shader_js.go | 26 +++++++ .../graphicsdriver/opengl/shader_notjs.go | 25 +++++++ internal/shader/shader_test.go | 7 +- internal/shaderir/glsl/glsl.go | 68 ++++++++++++++++--- internal/shaderir/glsl/type.go | 7 +- shader_test.go | 53 +++++++++++++++ 11 files changed, 217 insertions(+), 17 deletions(-) create mode 100644 internal/graphicsdriver/opengl/context_notwebgl1.go create mode 100644 internal/graphicsdriver/opengl/context_webgl1.go create mode 100644 internal/graphicsdriver/opengl/shader_js.go create mode 100644 internal/graphicsdriver/opengl/shader_notjs.go 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) + } + } + } +}