Files
nmage/assets/textures.go

336 lines
8.9 KiB
Go
Executable File

package assets
import (
"bytes"
"fmt"
"image"
"image/jpeg"
"image/png"
"io"
"os"
"path"
"strings"
"unsafe"
"github.com/go-gl/gl/v4.1-core/gl"
"github.com/mandykoh/prism"
)
type ColorFormat int
const (
ColorFormat_Unknown ColorFormat = iota
ColorFormat_RGBA8
)
var (
DefaultBlackTexId Texture
DefaultWhiteTexId Texture
DefaultDiffuseTexId Texture
DefaultSpecularTexId Texture
DefaultNormalTexId Texture
DefaultEmissionTexId Texture
)
type Texture struct {
// Path only exists for textures loaded from disk
Path string
TexID uint32
// Width is the width of the texture in pixels (pixels per row).
// Note that the number of bytes constituting a row is MORE than this (e.g. for RGBA8, bytesPerRow=width*4, since we have 4 bytes per pixel)
Width int32
// Height is the height of the texture in pixels (pixels per column).
// Note that the number of bytes constituting a column is MORE than this (e.g. for RGBA8, bytesPerColumn=height*4, since we have 4 bytes per pixel)
Height int32
// Pixels usually stored in RGBA format
Pixels []byte
}
type TextureLoadOptions struct {
TryLoadFromCache bool
WriteToCache bool
GenMipMaps bool
KeepPixelsInMem bool
NoSrgba bool
}
type Cubemap struct {
// These only exists for textures loaded from disk
RightPath string
LeftPath string
TopPath string
BotPath string
FrontPath string
BackPath string
TexID uint32
}
func LoadTexturePNG(file string, loadOptions *TextureLoadOptions) (Texture, error) {
if loadOptions == nil {
loadOptions = &TextureLoadOptions{}
}
if loadOptions.TryLoadFromCache {
if tex, ok := GetTextureFromCachePath(file); ok {
return tex, nil
}
}
//Load from disk
fileBytes, err := os.ReadFile(file)
if err != nil {
return Texture{}, err
}
bytesReader := bytes.NewReader(fileBytes)
img, err := png.Decode(bytesReader)
if err != nil {
return Texture{}, err
}
nrgbaImg := prism.ConvertImageToNRGBA(img, 2)
tex := Texture{
Path: file,
Pixels: nrgbaImg.Pix,
Width: int32(nrgbaImg.Bounds().Dx()),
Height: int32(nrgbaImg.Bounds().Dy()),
}
flipImgPixelsVertically(tex.Pixels, int(tex.Width), int(tex.Height), 4)
//Prepare opengl stuff
gl.GenTextures(1, &tex.TexID)
gl.BindTexture(gl.TEXTURE_2D, tex.TexID)
// set the texture wrapping/filtering options (on the currently bound texture object)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
// load and generate the texture
internalFormat := int32(gl.SRGB_ALPHA)
if loadOptions.NoSrgba {
internalFormat = gl.RGBA8
}
gl.TexImage2D(gl.TEXTURE_2D, 0, internalFormat, tex.Width, tex.Height, 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&tex.Pixels[0]))
if loadOptions.GenMipMaps {
gl.GenerateMipmap(tex.TexID)
}
if loadOptions.WriteToCache {
AddTextureToCache(tex)
}
if !loadOptions.KeepPixelsInMem {
tex.Pixels = nil
}
return tex, nil
}
func LoadTextureInMemPngImg(img image.Image, loadOptions *TextureLoadOptions) (Texture, error) {
if loadOptions == nil {
loadOptions = &TextureLoadOptions{}
}
nrgbaImg := prism.ConvertImageToNRGBA(img, 2)
tex := Texture{
Path: "",
Pixels: nrgbaImg.Pix,
Height: int32(nrgbaImg.Bounds().Dy()),
Width: int32(nrgbaImg.Bounds().Dx()),
}
flipImgPixelsVertically(tex.Pixels, int(tex.Width), int(tex.Height), 4)
//Prepare opengl stuff
gl.GenTextures(1, &tex.TexID)
gl.BindTexture(gl.TEXTURE_2D, tex.TexID)
// set the texture wrapping/filtering options (on the currently bound texture object)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
// load and generate the texture
internalFormat := int32(gl.SRGB_ALPHA)
if loadOptions.NoSrgba {
internalFormat = gl.RGBA8
}
gl.TexImage2D(gl.TEXTURE_2D, 0, internalFormat, tex.Width, tex.Height, 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&tex.Pixels[0]))
if loadOptions.GenMipMaps {
gl.GenerateMipmap(tex.TexID)
}
if loadOptions.WriteToCache {
AddTextureToCache(tex)
}
if !loadOptions.KeepPixelsInMem {
tex.Pixels = nil
}
return tex, nil
}
func LoadTextureJpeg(file string, loadOptions *TextureLoadOptions) (Texture, error) {
if loadOptions == nil {
loadOptions = &TextureLoadOptions{}
}
if loadOptions.TryLoadFromCache {
if tex, ok := GetTextureFromCachePath(file); ok {
return tex, nil
}
}
//Load from disk
fileBytes, err := os.ReadFile(file)
if err != nil {
return Texture{}, err
}
img, err := jpeg.Decode(bytes.NewReader(fileBytes))
if err != nil {
return Texture{}, err
}
nrgbaImg := prism.ConvertImageToNRGBA(img, 2)
tex := Texture{
Path: file,
Pixels: nrgbaImg.Pix,
Height: int32(nrgbaImg.Bounds().Dy()),
Width: int32(nrgbaImg.Bounds().Dx()),
}
flipImgPixelsVertically(tex.Pixels, int(tex.Width), int(tex.Height), 4)
//Prepare opengl stuff
gl.GenTextures(1, &tex.TexID)
gl.BindTexture(gl.TEXTURE_2D, tex.TexID)
// set the texture wrapping/filtering options (on the currently bound texture object)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
// load and generate the texture
internalFormat := int32(gl.SRGB_ALPHA)
if loadOptions.NoSrgba {
internalFormat = gl.RGBA8
}
gl.TexImage2D(gl.TEXTURE_2D, 0, internalFormat, tex.Width, tex.Height, 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&tex.Pixels[0]))
if loadOptions.GenMipMaps {
gl.GenerateMipmap(tex.TexID)
}
if loadOptions.WriteToCache {
AddTextureToCache(tex)
}
if !loadOptions.KeepPixelsInMem {
tex.Pixels = nil
}
return tex, nil
}
// LoadCubemapTextures only supports the 'TextureIsSrgba' option
func LoadCubemapTextures(rightTex, leftTex, topTex, botTex, frontTex, backTex string, loadOptions *TextureLoadOptions) (Cubemap, error) {
if loadOptions == nil {
loadOptions = &TextureLoadOptions{}
}
var imgDecoder func(r io.Reader) (image.Image, error)
ext := strings.ToLower(path.Ext(rightTex))
if ext == ".jpg" || ext == ".jpeg" {
imgDecoder = jpeg.Decode
} else if ext == ".png" {
imgDecoder = png.Decode
} else {
return Cubemap{}, fmt.Errorf("unknown image extension: %s. Expected one of: .jpg, .jpeg, .png", ext)
}
cmap := Cubemap{
RightPath: rightTex,
LeftPath: leftTex,
TopPath: topTex,
BotPath: botTex,
FrontPath: frontTex,
BackPath: backTex,
}
gl.GenTextures(1, &cmap.TexID)
gl.BindTexture(gl.TEXTURE_CUBE_MAP, cmap.TexID)
// The order here matters
texturePaths := []string{rightTex, leftTex, topTex, botTex, frontTex, backTex}
for i := uint32(0); i < uint32(len(texturePaths)); i++ {
fPath := texturePaths[i]
//Load from disk
fileBytes, err := os.ReadFile(fPath)
if err != nil {
return Cubemap{}, err
}
img, err := imgDecoder(bytes.NewReader(fileBytes))
if err != nil {
return Cubemap{}, err
}
nrgbaImg := prism.ConvertImageToNRGBA(img, 2)
height := int32(nrgbaImg.Bounds().Dy())
width := int32(nrgbaImg.Bounds().Dx())
internalFormat := int32(gl.SRGB_ALPHA)
if loadOptions.NoSrgba {
internalFormat = gl.RGBA8
}
gl.TexImage2D(uint32(gl.TEXTURE_CUBE_MAP_POSITIVE_X)+i, 0, internalFormat, int32(width), int32(height), 0, gl.RGBA, gl.UNSIGNED_BYTE, unsafe.Pointer(&nrgbaImg.Pix[0]))
}
// set the texture wrapping/filtering options (on the currently bound texture object)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE)
return cmap, nil
}
func flipImgPixelsVertically(bytes []byte, width, height, bytesPerPixel int) {
// Flip the image vertically such that (e.g. in an image of 10 rows) rows 0<->9, 1<->8, 2<->7 etc are swapped.
// We do this because images are usually stored top-left to bottom-right, while opengl stores textures bottom-left to top-right, so if we don't swap
// rows textures will appear inverted
widthInBytes := width * bytesPerPixel
rowData := make([]byte, width*bytesPerPixel)
for rowNum := 0; rowNum < height/2; rowNum++ {
upperRowStartIndex := rowNum * widthInBytes
lowerRowStartIndex := (height - rowNum - 1) * widthInBytes
copy(rowData, bytes[upperRowStartIndex:upperRowStartIndex+widthInBytes])
copy(bytes[upperRowStartIndex:upperRowStartIndex+widthInBytes], bytes[lowerRowStartIndex:lowerRowStartIndex+widthInBytes])
copy(bytes[lowerRowStartIndex:lowerRowStartIndex+widthInBytes], rowData)
}
}