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  )