Files
radiantwave/archive/internal/shaderManager/shaderManager.go
T
2026-08-07 14:51:41 -05:00

200 lines
5.4 KiB
Go

package shaderManager
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"github.com/go-gl/gl/v3.3-core/gl"
)
// Shader represents a compiled GLSL program and its associated data.
type Shader struct {
Name string
ProgramID uint32
Render func() // This will be used in the future.
// Cached uniform locations (looked up once after linking)
LocMVPMatrix int32
LocTexture int32
LocTextColor int32
LocColor int32
}
// ShaderManager handles the loading, compiling, and management of GLSL shader programs.
type ShaderManager struct {
shaders map[string]*Shader
}
var (
instance *ShaderManager
once sync.Once
)
// InitShaderManager initializes the ShaderManager singleton.
func InitShaderManager(shaderDir string) error {
var err error
once.Do(func() {
sm := &ShaderManager{
shaders: make(map[string]*Shader),
}
if e := sm.LoadShaders(shaderDir); e != nil {
err = fmt.Errorf("could not load shaders from %s: %w", shaderDir, e)
return
}
instance = sm
})
return err
}
// Get returns the singleton ShaderManager instance.
func Get() *ShaderManager {
if instance == nil {
panic("ShaderManager has not been initialized. Call InitShaderManager at application startup.")
}
return instance
}
// Get retrieves a compiled Shader object by its name.
func (sm *ShaderManager) Get(name string) (*Shader, bool) {
shader, ok := sm.shaders[name]
return shader, ok
}
// ListShaders returns all shaders.
func (sm *ShaderManager) ListShaders() map[string]*Shader {
return sm.shaders
}
// Close deletes all loaded shader programs from the GPU to free up resources.
func (sm *ShaderManager) Close() {
for name, shader := range sm.shaders {
gl.DeleteProgram(shader.ProgramID)
delete(sm.shaders, name)
}
}
// LoadShaders scans a directory for .vert and .frag files and creates a shader program for each pair.
func (sm *ShaderManager) LoadShaders(shaderDir string) error {
// Get assets directory from database
shaderFiles := make(map[string]string)
err := filepath.Walk(shaderDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasSuffix(info.Name(), ".vert") {
name := strings.TrimSuffix(info.Name(), ".vert")
shaderFiles[name] = path
}
return nil
})
if err != nil {
return fmt.Errorf("error walking shader directory: %w", err)
}
for name, vertPath := range shaderFiles {
fragPath := strings.Replace(vertPath, ".vert", ".frag", 1)
if _, err := os.Stat(fragPath); os.IsNotExist(err) {
continue
}
programID, err := createShaderProgram(vertPath, fragPath)
if err != nil {
continue
}
newShader := &Shader{
Name: name,
ProgramID: programID,
Render: nil, // Render function is not yet assigned.
}
newShader.cacheUniformLocations()
sm.shaders[name] = newShader
}
return nil
}
// cacheUniformLocations looks up common uniform locations once and stores them on the Shader.
func (s *Shader) cacheUniformLocations() {
s.LocMVPMatrix = gl.GetUniformLocation(s.ProgramID, gl.Str("u_mvpMatrix\x00"))
s.LocTexture = gl.GetUniformLocation(s.ProgramID, gl.Str("u_texture\x00"))
s.LocTextColor = gl.GetUniformLocation(s.ProgramID, gl.Str("u_textColor\x00"))
s.LocColor = gl.GetUniformLocation(s.ProgramID, gl.Str("u_color\x00"))
}
// createShaderProgram reads, compiles, and links a vertex and fragment shader from file paths.
func createShaderProgram(vertexPath, fragmentPath string) (uint32, error) {
vertexSource, err := os.ReadFile(vertexPath)
if err != nil {
return 0, fmt.Errorf("reading vertex shader %s: %w", vertexPath, err)
}
fragmentSource, err := os.ReadFile(fragmentPath)
if err != nil {
return 0, fmt.Errorf("reading fragment shader %s: %w", fragmentPath, err)
}
vertexShader, err := compileShader(string(vertexSource)+"\x00", gl.VERTEX_SHADER)
if err != nil {
return 0, err
}
defer gl.DeleteShader(vertexShader)
fragmentShader, err := compileShader(string(fragmentSource)+"\x00", gl.FRAGMENT_SHADER)
if err != nil {
return 0, err
}
defer gl.DeleteShader(fragmentShader)
program := gl.CreateProgram()
gl.AttachShader(program, vertexShader)
gl.AttachShader(program, fragmentShader)
gl.LinkProgram(program)
var status int32
gl.GetProgramiv(program, gl.LINK_STATUS, &status)
if status == gl.FALSE {
var logLength int32
gl.GetProgramiv(program, gl.INFO_LOG_LENGTH, &logLength)
log := strings.Repeat("\x00", int(logLength+1))
gl.GetProgramInfoLog(program, logLength, nil, gl.Str(log))
return 0, fmt.Errorf("failed to link program: %v", log)
}
return program, nil
}
// compileShader is a helper function that compiles a single shader source string.
func compileShader(source string, shaderType uint32) (uint32, error) {
shader := gl.CreateShader(shaderType)
csources, free := gl.Strs(source)
gl.ShaderSource(shader, 1, csources, nil)
free()
gl.CompileShader(shader)
var status int32
gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)
if status == gl.FALSE {
var logLength int32
gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)
log := strings.Repeat("\x00", int(logLength+1))
gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log))
var shaderTypeName string
switch shaderType {
case gl.VERTEX_SHADER:
shaderTypeName = "Vertex"
case gl.FRAGMENT_SHADER:
shaderTypeName = "Fragment"
default:
shaderTypeName = "Unknown"
}
return 0, fmt.Errorf("failed to compile %s shader: %v", shaderTypeName, log)
}
return shader, nil
}