github.com/cybriq/giocore@v0.0.7-0.20210703034601-cfb9cb5f3900/gpu/internal/opengl/srgb.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package opengl 4 5 import ( 6 "errors" 7 "fmt" 8 "image" 9 "runtime" 10 "strings" 11 12 "github.com/cybriq/giocore/internal/byteslice" 13 "github.com/cybriq/giocore/internal/gl" 14 ) 15 16 // SRGBFBO implements an intermediate sRGB FBO 17 // for gamma-correct rendering on platforms without 18 // sRGB enabled native framebuffers. 19 type SRGBFBO struct { 20 c *gl.Functions 21 state *glState 22 viewport image.Point 23 srgbBuffer gl.Framebuffer 24 depthBuffer gl.Renderbuffer 25 colorTex gl.Texture 26 blitted bool 27 quad gl.Buffer 28 prog gl.Program 29 gl3 bool 30 } 31 32 func NewSRGBFBO(f *gl.Functions, state *glState) (*SRGBFBO, error) { 33 var gl3 bool 34 glVer := f.GetString(gl.VERSION) 35 ver, _, err := gl.ParseGLVersion(glVer) 36 if err != nil { 37 return nil, err 38 } 39 if ver[0] >= 3 { 40 gl3 = true 41 } else { 42 exts := f.GetString(gl.EXTENSIONS) 43 if !strings.Contains(exts, "EXT_sRGB") { 44 return nil, fmt.Errorf("no support for OpenGL ES 3 nor EXT_sRGB") 45 } 46 } 47 s := &SRGBFBO{ 48 c: f, 49 state: state, 50 gl3: gl3, 51 srgbBuffer: f.CreateFramebuffer(), 52 colorTex: f.CreateTexture(), 53 depthBuffer: f.CreateRenderbuffer(), 54 } 55 state.bindTexture(f, 0, s.colorTex) 56 f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) 57 f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) 58 f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) 59 f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) 60 return s, nil 61 } 62 63 func (s *SRGBFBO) Blit() { 64 if !s.blitted { 65 prog, err := gl.CreateProgram(s.c, blitVSrc, blitFSrc, []string{"pos", "uv"}) 66 if err != nil { 67 panic(err) 68 } 69 s.prog = prog 70 s.state.useProgram(s.c, prog) 71 s.c.Uniform1i(s.c.GetUniformLocation(prog, "tex"), 0) 72 s.quad = s.c.CreateBuffer() 73 s.state.bindBuffer(s.c, gl.ARRAY_BUFFER, s.quad) 74 coords := byteslice.Slice([]float32{ 75 -1, +1, 0, 1, 76 +1, +1, 1, 1, 77 -1, -1, 0, 0, 78 +1, -1, 1, 0, 79 }) 80 s.c.BufferData(gl.ARRAY_BUFFER, len(coords), gl.STATIC_DRAW) 81 s.c.BufferSubData(gl.ARRAY_BUFFER, 0, coords) 82 s.blitted = true 83 } 84 s.state.useProgram(s.c, s.prog) 85 s.state.bindTexture(s.c, 0, s.colorTex) 86 s.state.vertexAttribPointer(s.c, s.quad, 0 /* pos */, 2, gl.FLOAT, false, 4*4, 0) 87 s.state.vertexAttribPointer(s.c, s.quad, 1 /* uv */, 2, gl.FLOAT, false, 4*4, 4*2) 88 s.state.setVertexAttribArray(s.c, 0, true) 89 s.state.setVertexAttribArray(s.c, 1, true) 90 s.c.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) 91 s.state.bindFramebuffer(s.c, gl.FRAMEBUFFER, s.srgbBuffer) 92 s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0) 93 s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT) 94 } 95 96 func (s *SRGBFBO) Framebuffer() gl.Framebuffer { 97 return s.srgbBuffer 98 } 99 100 func (s *SRGBFBO) Refresh(viewport image.Point) error { 101 if viewport.X == 0 || viewport.Y == 0 { 102 return errors.New("srgb: zero-sized framebuffer") 103 } 104 if s.viewport == viewport { 105 return nil 106 } 107 s.viewport = viewport 108 s.state.bindTexture(s.c, 0, s.colorTex) 109 if s.gl3 { 110 s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.SRGB8_ALPHA8, viewport.X, viewport.Y, gl.RGBA, gl.UNSIGNED_BYTE) 111 } else /* EXT_sRGB */ { 112 s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.SRGB_ALPHA_EXT, viewport.X, viewport.Y, gl.SRGB_ALPHA_EXT, gl.UNSIGNED_BYTE) 113 } 114 s.state.bindRenderbuffer(s.c, gl.RENDERBUFFER, s.depthBuffer) 115 s.c.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, viewport.X, viewport.Y) 116 s.state.bindFramebuffer(s.c, gl.FRAMEBUFFER, s.srgbBuffer) 117 s.c.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, s.colorTex, 0) 118 s.c.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, s.depthBuffer) 119 if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { 120 return fmt.Errorf("sRGB framebuffer incomplete (%dx%d), status: %#x error: %x", viewport.X, viewport.Y, st, s.c.GetError()) 121 } 122 123 if runtime.GOOS == "js" { 124 // With macOS Safari, rendering to and then reading from a SRGB8_ALPHA8 125 // texture result in twice gamma corrected colors. Using a plain RGBA 126 // texture seems to work. 127 s.state.setClearColor(s.c, .5, .5, .5, 1.0) 128 s.c.Clear(gl.COLOR_BUFFER_BIT) 129 var pixel [4]byte 130 s.c.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel[:]) 131 if pixel[0] == 128 { // Correct sRGB color value is ~188 132 s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, viewport.X, viewport.Y, gl.RGBA, gl.UNSIGNED_BYTE) 133 if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { 134 return fmt.Errorf("fallback RGBA framebuffer incomplete (%dx%d), status: %#x error: %x", viewport.X, viewport.Y, st, s.c.GetError()) 135 } 136 } 137 } 138 139 return nil 140 } 141 142 func (s *SRGBFBO) Release() { 143 s.state.deleteFramebuffer(s.c, s.srgbBuffer) 144 s.state.deleteTexture(s.c, s.colorTex) 145 s.state.deleteRenderbuffer(s.c, s.depthBuffer) 146 if s.blitted { 147 s.state.deleteBuffer(s.c, s.quad) 148 s.state.deleteProgram(s.c, s.prog) 149 } 150 s.c = nil 151 } 152 153 const ( 154 blitVSrc = ` 155 #version 100 156 157 precision highp float; 158 159 attribute vec2 pos; 160 attribute vec2 uv; 161 162 varying vec2 vUV; 163 164 void main() { 165 gl_Position = vec4(pos, 0, 1); 166 vUV = uv; 167 } 168 ` 169 blitFSrc = ` 170 #version 100 171 172 precision mediump float; 173 174 uniform sampler2D tex; 175 varying vec2 vUV; 176 177 vec3 gamma(vec3 rgb) { 178 vec3 exp = vec3(1.055)*pow(rgb, vec3(0.41666)) - vec3(0.055); 179 vec3 lin = rgb * vec3(12.92); 180 bvec3 cut = lessThan(rgb, vec3(0.0031308)); 181 return vec3(cut.r ? lin.r : exp.r, cut.g ? lin.g : exp.g, cut.b ? lin.b : exp.b); 182 } 183 184 void main() { 185 vec4 col = texture2D(tex, vUV); 186 vec3 rgb = col.rgb; 187 rgb = gamma(rgb); 188 gl_FragColor = vec4(rgb, col.a); 189 } 190 ` 191 )