Performance and QoL improvements.

PERFORMANCE: Removing color and clipAlpha intermediate buffers. Rendering now happens directly on the results textures.
Moving shaders over to use pixels mode instead of texels mode.
Known Issue: All textures now loop because we always use shaders now, regardless of the Material's texture wrap mode. This being the case, I've unexported that variable.
Custom fragment shaders have been reworked to both require unit-mode Kage shaders, and allow combination with Tetra3d's built-in 3D depth-testing and fog-applying shader. To do this, simply rename Fragment() to CustomFragment() in your shader, and it will slot in after depth-testing and before fog application.
DrawSpriteIn3D() has also been updated for these changes.
perspectiveCorrected
solarlune 2023-10-15 23:51:47 -07:00
parent 5d4bb14628
commit 782f7f3b38
9 changed files with 423 additions and 261 deletions

377
camera.go
View File

@ -52,12 +52,10 @@ type Camera struct {
// then only that many lights will be considered. If less than or equal to 0 (the default), then all available lights will be used.
MaxLightCount int
resultColorTexture *ebiten.Image // ColorTexture holds the color results of rendering any models.
resultDepthTexture *ebiten.Image // DepthTexture holds the depth results of rendering any models, if Camera.RenderDepth is on.
resultNormalTexture *ebiten.Image // NormalTexture holds a texture indicating the normal render
colorIntermediate *ebiten.Image
depthIntermediate *ebiten.Image
clipAlphaIntermediate *ebiten.Image
resultColorTexture *ebiten.Image // ColorTexture holds the color results of rendering any models.
resultDepthTexture *ebiten.Image // DepthTexture holds the depth results of rendering any models, if Camera.RenderDepth is on.
resultNormalTexture *ebiten.Image // NormalTexture holds a texture indicating the normal render
depthIntermediate *ebiten.Image
resultAccumulatedColorTexture *ebiten.Image // ResultAccumulatedColorTexture holds the previous frame's render result of rendering any models.
accumulatedBackBuffer *ebiten.Image
@ -73,11 +71,10 @@ type Camera struct {
DebugInfo DebugInfo
depthShader *ebiten.Shader
clipAlphaCompositeShader *ebiten.Shader
clipAlphaRenderShader *ebiten.Shader
colorShader *ebiten.Shader
sprite3DShader *ebiten.Shader
depthShader *ebiten.Shader
clipAlphaShader *ebiten.Shader
colorShader *ebiten.Shader
sprite3DShader *ebiten.Shader
// Visibility check variables
cameraForward Vector
@ -121,6 +118,8 @@ func NewCamera(w, h int) *Camera {
depthShaderText := []byte(
`package main
//kage:unit pixels
func encodeDepth(depth float) vec4 {
r := floor(depth * 255) / 255
g := floor(fract(depth * 255) * 255) / 255
@ -132,9 +131,13 @@ func NewCamera(w, h int) *Camera {
return rgba.r + (rgba.g / 255) + (rgba.b / 65025)
}
func Fragment(position vec4, texCoord vec2, color vec4) vec4 {
func dstPosToSrcPos(dstPos vec2) vec2 {
return dstPos.xy - imageDstOrigin() + imageSrc0Origin()
}
existingDepth := imageSrc0At(position.xy / imageSrcTextureSize())
func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
existingDepth := imageSrc0UnsafeAt(dstPosToSrcPos(dstPos.xy))
if existingDepth.a == 0 || decodeDepth(existingDepth) > color.r {
return encodeDepth(color.r)
@ -166,15 +169,22 @@ func NewCamera(w, h int) *Camera {
return vec4(r, g, b, 1);
}
func decodeDepth(rgba vec4) float {
return rgba.r + (rgba.g / 255) + (rgba.b / 65025)
}
func dstPosToSrcPos(dstPos vec2) vec2 {
return dstPos.xy - imageDstOrigin() + imageSrc0Origin()
}
func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
srcOrigin := imageSrc0Origin()
srcSize := imageSrc0Size()
srcSize := imageSrc1Size()
// There's atlassing going on behind the scenes here, so:
// Subtract the source position by the src texture's origin on the atlas.
// This gives us the actual pixel coordinates.
tx := srcPos - srcOrigin
tx := srcPos
// Divide by the source image size to get the UV coordinates.
tx /= srcSize
@ -185,39 +195,16 @@ func NewCamera(w, h int) *Camera {
// Multiply by the size to get the pixel coordinates again.
tx *= srcSize
// Add the origin back in to get the texture coordinates that Kage expects.
tex := imageSrc0UnsafeAt(tx + srcOrigin)
tex := imageSrc1UnsafeAt(tx)
if (tex.a == 0) {
discard()
}
return vec4(encodeDepth(color.r).rgb, tex.a)
}
depthValue := imageSrc0UnsafeAt(dstPosToSrcPos(dstPos.xy))
`,
)
cam.clipAlphaRenderShader, err = ebiten.NewShader(clipAlphaShaderText)
if err != nil {
panic(err)
}
clipCompositeShaderText := []byte(
`package main
func decodeDepth(rgba vec4) float {
return rgba.r + (rgba.g / 255) + (rgba.b / 65025)
}
func Fragment(position vec4, texCoord vec2, color vec4) vec4 {
depthValue := imageSrc0UnsafeAt(texCoord)
texture := imageSrc1UnsafeAt(texCoord)
if depthValue.a == 0 || decodeDepth(depthValue) > texture.r {
return texture
if depthValue.a == 0 || decodeDepth(depthValue) > color.r {
return vec4(encodeDepth(color.r).rgb, tex.a)
}
discard()
@ -227,100 +214,13 @@ func NewCamera(w, h int) *Camera {
`,
)
cam.clipAlphaCompositeShader, err = ebiten.NewShader(clipCompositeShaderText)
cam.clipAlphaShader, err = ebiten.NewShader(clipAlphaShaderText)
if err != nil {
panic(err)
}
cam.colorShader, err = ebiten.NewShader([]byte(
`package main
var Fog vec4
var FogRange [2]float
var DitherSize float
var FogCurve float
var Fogless float
var BayerMatrix [16]float
func decodeDepth(rgba vec4) float {
return rgba.r + (rgba.g / 255) + (rgba.b / 65025)
}
func OutCirc(v float) float {
return sqrt(1 - pow(v - 1, 2))
}
func InCirc(v float) float {
return 1 - sqrt(1 - pow(v, 2))
}
func Fragment(position vec4, texCoord vec2, color vec4) vec4 {
depth := imageSrc1UnsafeAt(texCoord)
if depth.a > 0 {
colorTex := imageSrc0UnsafeAt(texCoord)
if Fogless == 0 {
var d float
if FogCurve == 0 {
d = smoothstep(FogRange[0], FogRange[1], decodeDepth(depth))
} else if FogCurve == 1 {
d = smoothstep(FogRange[0], FogRange[1], OutCirc(decodeDepth(depth)))
} else if FogCurve == 2 {
d = smoothstep(FogRange[0], FogRange[1], InCirc(decodeDepth(depth)))
}
if DitherSize > 0 {
yc := int(position.y / DitherSize)%4
xc := int(position.x / DitherSize)%4
fogMult := step(0, d - BayerMatrix[(yc*4) + xc])
// Fog mode is 4th channel in Fog vector ([4]float32)
if Fog.a == 0 {
colorTex.rgb += Fog.rgb * fogMult * colorTex.a
} else if Fog.a == 1 {
colorTex.rgb -= Fog.rgb * fogMult * colorTex.a
} else if Fog.a == 2 {
colorTex.rgb = mix(colorTex.rgb, Fog.rgb, fogMult) * colorTex.a
} else if Fog.a == 3 {
colorTex.a *= abs(1-d) * step(0, abs(1-d) - BayerMatrix[(yc*4) + xc])
colorTex.rgb = mix(vec3(0, 0, 0), colorTex.rgb, colorTex.a)
}
} else {
if Fog.a == 0 {
colorTex.rgb += Fog.rgb * d * colorTex.a
} else if Fog.a == 1 {
colorTex.rgb -= Fog.rgb * d * colorTex.a
} else if Fog.a == 2 {
colorTex.rgb = mix(colorTex.rgb, Fog.rgb, d) * colorTex.a
} else if Fog.a == 3 {
colorTex.a *= abs(1-d)
colorTex.rgb = mix(vec3(0, 0, 0), colorTex.rgb, colorTex.a)
}
}
}
return colorTex
}
// This should be discard as well, rather than alpha 0
discard()
}
`,
))
cam.colorShader, err = ExtendBase3DShader("")
if err != nil {
panic(err)
@ -328,6 +228,7 @@ func NewCamera(w, h int) *Camera {
sprite3DShaderText := []byte(
`package main
//kage:unit pixels
var SpriteDepth float
@ -335,12 +236,16 @@ func NewCamera(w, h int) *Camera {
return rgba.r + (rgba.g / 255) + (rgba.b / 65025)
}
func Fragment(position vec4, texCoord vec2, color vec4) vec4 {
func dstPosToSrcPos(dstPos vec2) vec2 {
return dstPos.xy - imageDstOrigin() + imageSrc0Origin()
}
resultDepth := imageSrc1UnsafeAt(texCoord)
func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
resultDepth := imageSrc1UnsafeAt(dstPosToSrcPos(dstPos.xy))
if resultDepth.a == 0 || decodeDepth(resultDepth) > SpriteDepth {
return imageSrc0UnsafeAt(texCoord)
return imageSrc0UnsafeAt(srcPos)
}
discard()
@ -408,9 +313,7 @@ func (camera *Camera) Resize(w, h int) {
camera.resultNormalTexture.Dispose()
camera.accumulatedBackBuffer.Dispose()
camera.resultDepthTexture.Dispose()
camera.colorIntermediate.Dispose()
camera.depthIntermediate.Dispose()
camera.clipAlphaIntermediate.Dispose()
}
bounds := image.Rect(0, 0, w, h)
@ -422,9 +325,7 @@ func (camera *Camera) Resize(w, h int) {
camera.resultColorTexture = ebiten.NewImageWithOptions(bounds, opt)
camera.resultDepthTexture = ebiten.NewImageWithOptions(bounds, opt)
camera.resultNormalTexture = ebiten.NewImageWithOptions(bounds, opt)
camera.colorIntermediate = ebiten.NewImageWithOptions(bounds, opt)
camera.depthIntermediate = ebiten.NewImageWithOptions(bounds, opt)
camera.clipAlphaIntermediate = ebiten.NewImageWithOptions(bounds, opt)
camera.sphereFactorCalculated = false
camera.updateProjectionMatrix = true
@ -1045,28 +946,7 @@ func (camera *Camera) Render(scene *Scene, lights []ILight, models ...*Model) {
// matrix, which we feed into model.TransformedVertices() to draw vertices in order of distance.
vpMatrix := camera.ViewMatrix().Mult(camera.Projection())
rectShaderOptions := &ebiten.DrawRectShaderOptions{}
rectShaderOptions.Images[0] = camera.colorIntermediate
rectShaderOptions.Images[1] = camera.depthIntermediate
if scene != nil && scene.World != nil {
rectShaderOptions.Uniforms = map[string]interface{}{
"Fog": scene.World.fogAsFloatSlice(),
"FogRange": scene.World.FogRange,
"DitherSize": scene.World.DitheredFogSize,
"FogCurve": float32(scene.World.FogCurve),
"BayerMatrix": bayerMatrix,
}
} else {
rectShaderOptions.Uniforms = map[string]interface{}{
"Fog": []float32{0, 0, 0, 0},
"FogRange": []float32{0, 1},
}
}
colorPassShaderOptions := &ebiten.DrawTrianglesShaderOptions{}
// Reusing vectors rather than reallocating for all triangles for all models
solids := []renderPair{}
@ -1530,14 +1410,10 @@ func (camera *Camera) Render(scene *Scene, lights []ILight, models ...*Model) {
if transparencyMode == TransparencyModeAlphaClip {
camera.clipAlphaIntermediate.Clear()
camera.clipAlphaIntermediate.DrawTrianglesShader(depthVertexList[:vertexListIndex], indexList[:indexListIndex], camera.clipAlphaRenderShader, &ebiten.DrawTrianglesShaderOptions{Images: [4]*ebiten.Image{img}})
w := camera.depthIntermediate.Bounds().Dx()
h := camera.depthIntermediate.Bounds().Dy()
camera.depthIntermediate.DrawRectShader(w, h, camera.clipAlphaCompositeShader, &ebiten.DrawRectShaderOptions{Images: [4]*ebiten.Image{camera.resultDepthTexture, camera.clipAlphaIntermediate}})
shaderOpt := &ebiten.DrawTrianglesShaderOptions{
Images: [4]*ebiten.Image{camera.resultDepthTexture, img},
}
camera.depthIntermediate.DrawTrianglesShader(depthVertexList[:vertexListIndex], indexList[:indexListIndex], camera.clipAlphaShader, shaderOpt)
} else {
shaderOpt := &ebiten.DrawTrianglesShaderOptions{
@ -1553,65 +1429,107 @@ func (camera *Camera) Render(scene *Scene, lights []ILight, models ...*Model) {
}
t := &ebiten.DrawTrianglesOptions{}
colorPassOptions := &ebiten.DrawTrianglesOptions{}
if model.ColorBlendingFunc != nil {
t.ColorM = model.ColorBlendingFunc(model, meshPart) // Modify the model's appearance using its color blending function
colorPassOptions.ColorM = model.ColorBlendingFunc(model, meshPart) // Modify the model's appearance using its color blending function
}
if mat != nil {
t.Filter = mat.TextureFilterMode
t.Address = mat.TextureWrapMode
colorPassOptions.Filter = mat.TextureFilterMode
colorPassOptions.Address = mat.textureWrapMode
}
hasFragShader := mat != nil && mat.fragmentShader != nil && mat.FragmentShaderOn
w, h := camera.resultColorTexture.Size()
// If rendering depth, and rendering through a custom fragment shader, we'll need to render the tris to the ColorIntermediate buffer using the custom shader.
// If we're not rendering through a custom shader, we can render to ColorIntermediate and then composite that onto the finished ColorTexture.
// If we're not rendering depth, but still rendering through the shader, we can render to the intermediate texture, and then from there composite.
// Otherwise, we can just draw the triangles normally.
if mat != nil {
colorPassShaderOptions.CompositeMode = mat.CompositeMode
}
if scene != nil && scene.World != nil {
colorPassShaderOptions.Uniforms = map[string]interface{}{
"Fog": scene.World.fogAsFloatSlice(),
"FogRange": scene.World.FogRange,
"DitherSize": scene.World.DitheredFogSize,
"FogCurve": float32(scene.World.FogCurve),
"BayerMatrix": bayerMatrix,
}
} else {
colorPassShaderOptions.Uniforms = map[string]interface{}{
"Fog": []float32{0, 0, 0, 0},
"FogRange": []float32{0, 1},
}
}
colorPassShaderOptions.Images[0] = img
colorPassShaderOptions.Images[1] = camera.depthIntermediate
colorPassOptions.CompositeMode = ebiten.CompositeModeSourceOver
colorPassShaderOptions.CompositeMode = ebiten.CompositeModeSourceOver
fogless := float32(0)
if mat != nil && mat.Fogless {
fogless = 1
}
rectShaderOptions.Uniforms["Fogless"] = fogless
t.CompositeMode = ebiten.CompositeModeSourceOver
rectShaderOptions.CompositeMode = ebiten.CompositeModeSourceOver
if camera.RenderNormals {
camera.colorIntermediate.Clear()
camera.colorIntermediate.DrawTriangles(normalVertexList[:vertexListIndex], indexList[:indexListIndex], img, t)
camera.resultNormalTexture.DrawRectShader(w, h, camera.colorShader, rectShaderOptions)
colorPassShaderOptions.Images[0] = defaultImg
colorPassShaderOptions.Uniforms["Fogless"] = 1 // No fog in a normal render
camera.resultNormalTexture.DrawTrianglesShader(normalVertexList[:vertexListIndex], indexList[:indexListIndex], camera.colorShader, colorPassShaderOptions)
// camera.resultNormalTexture.DrawTrianglesShader(colorVertexList[:vertexListIndex], indexList[:indexListIndex], camera.colorShader, colorPassShaderOptions)
} else {
colorPassShaderOptions.Uniforms["Fogless"] = fogless
}
if camera.RenderDepth {
camera.colorIntermediate.Clear()
if mat != nil {
rectShaderOptions.CompositeMode = mat.CompositeMode
}
if hasFragShader {
camera.colorIntermediate.DrawTrianglesShader(colorVertexList[:vertexListIndex], indexList[:indexListIndex], mat.fragmentShader, mat.FragmentShaderOptions)
if mat.FragmentShaderOptions != nil {
colorPassShaderOptions.Blend = mat.FragmentShaderOptions.Blend
colorPassShaderOptions.AntiAlias = mat.FragmentShaderOptions.AntiAlias
colorPassShaderOptions.FillRule = mat.FragmentShaderOptions.FillRule
colorPassShaderOptions.CompositeMode = mat.FragmentShaderOptions.CompositeMode
for k, v := range mat.FragmentShaderOptions.Uniforms {
colorPassShaderOptions.Uniforms[k] = v
}
if len(mat.FragmentShaderOptions.Images) > 0 && mat.FragmentShaderOptions.Images[0] != nil {
colorPassShaderOptions.Images[0] = mat.FragmentShaderOptions.Images[0]
}
if len(mat.FragmentShaderOptions.Images) > 1 && mat.FragmentShaderOptions.Images[1] != nil {
colorPassShaderOptions.Images[1] = mat.FragmentShaderOptions.Images[1]
}
if len(mat.FragmentShaderOptions.Images) > 2 && mat.FragmentShaderOptions.Images[2] != nil {
colorPassShaderOptions.Images[2] = mat.FragmentShaderOptions.Images[2]
}
if len(mat.FragmentShaderOptions.Images) > 3 && mat.FragmentShaderOptions.Images[3] != nil {
colorPassShaderOptions.Images[3] = mat.FragmentShaderOptions.Images[3]
}
}
camera.resultColorTexture.DrawTrianglesShader(colorVertexList[:vertexListIndex], indexList[:indexListIndex], mat.fragmentShader, colorPassShaderOptions)
} else {
camera.colorIntermediate.DrawTriangles(colorVertexList[:vertexListIndex], indexList[:indexListIndex], img, t)
camera.resultColorTexture.DrawTrianglesShader(colorVertexList[:vertexListIndex], indexList[:indexListIndex], camera.colorShader, colorPassShaderOptions)
}
camera.resultColorTexture.DrawRectShader(w, h, camera.colorShader, rectShaderOptions)
// camera.resultColorTexture.DrawRectShader(w, h, camera.colorShader, rectShaderOptions)
} else {
if mat != nil {
t.CompositeMode = mat.CompositeMode
colorPassOptions.CompositeMode = mat.CompositeMode
}
if hasFragShader {
camera.resultColorTexture.DrawTrianglesShader(colorVertexList[:vertexListIndex], indexList[:indexListIndex], mat.fragmentShader, mat.FragmentShaderOptions)
} else {
camera.resultColorTexture.DrawTriangles(colorVertexList[:vertexListIndex], indexList[:indexListIndex], img, t)
camera.resultColorTexture.DrawTriangles(colorVertexList[:vertexListIndex], indexList[:indexListIndex], img, colorPassOptions)
}
}
@ -1748,30 +1666,48 @@ func (camera *Camera) Render(scene *Scene, lights []ILight, models ...*Model) {
// }
type SpriteRender3d struct {
Image *ebiten.Image
Options *ebiten.DrawImageOptions
WorldPosition Vector
type DrawSprite3dSettings struct {
// The image to render
Image *ebiten.Image
// Options for drawing the sprite; defaults to nil, which is default settings.
Options *ebiten.DrawTrianglesShaderOptions
// How much to offset the depth - useful if you want the object to appear at a position,
// but in front of or behind other objects. Negative is towards the camera, positive is away.
// The offset ranges from 0 to 1.
DepthOffset float32
WorldPosition Vector // The position of the sprite in 3D space
}
// DrawImageIn3D draws an image on the screen in 2D, but at the screen position of the 3D world position provided
var spriteRender3DVerts = []ebiten.Vertex{
{DstX: 0, DstY: 0, SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
{DstX: 0, DstY: 0, SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
{DstX: 0, DstY: 0, SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
{DstX: 0, DstY: 0, SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
}
var spriteRender3DIndices = []uint16{
0, 1, 2,
2, 3, 0,
}
// DrawSprite3D draws an image on the screen in 2D, but at the screen position of the 3D world position provided,
// and with depth intersection.
// This allows you to render 2D elements "at" a 3D position, and can be very useful in situations where you want
// a sprite to render at 100% size and no perspective or skewing, but still look like it's in the 3D space (like in
// a game with a fixed camera viewpoint).
func (camera *Camera) DrawImageIn3D(screen *ebiten.Image, renderSettings ...SpriteRender3d) {
func (camera *Camera) DrawSprite3D(screen *ebiten.Image, renderSettings ...DrawSprite3dSettings) {
// TODO: Replace this with a more performant alternative, where we minimize shader / texture switches.
depthMarginPercentage := (camera.far - camera.near) * camera.DepthMargin
for _, rs := range renderSettings {
camViewProj := camera.ViewMatrix().Mult(camera.Projection())
camera.colorIntermediate.Clear()
for _, rs := range renderSettings {
px := camera.WorldToScreen(rs.WorldPosition)
out := camera.ViewMatrix().Mult(camera.Projection()).MultVec(rs.WorldPosition)
out := camViewProj.MultVec(rs.WorldPosition)
depth := float32((out.Z + depthMarginPercentage) / (camera.far - camera.near + (depthMarginPercentage * 2)))
@ -1782,23 +1718,36 @@ func (camera *Camera) DrawImageIn3D(screen *ebiten.Image, renderSettings ...Spri
depth = 1
}
opt := rs.Options
depth += float32(rs.DepthOffset)
if opt == nil {
opt = &ebiten.DrawImageOptions{}
}
opt.GeoM.Translate(px.X, px.Y)
imageW := float32(rs.Image.Bounds().Dx())
imageH := float32(rs.Image.Bounds().Dy())
halfImageW := imageW / 2
halfImageH := imageH / 2
camera.colorIntermediate.DrawImage(rs.Image, opt)
spriteRender3DVerts[0].DstX = float32(px.X) - halfImageW
spriteRender3DVerts[0].DstY = float32(px.Y) - halfImageH
colorTextureW, colorTextureH := camera.resultColorTexture.Size()
rectShaderOptions := &ebiten.DrawRectShaderOptions{}
rectShaderOptions.Images[0] = camera.colorIntermediate
rectShaderOptions.Images[1] = camera.resultDepthTexture
rectShaderOptions.Uniforms = map[string]interface{}{
spriteRender3DVerts[1].DstX = float32(px.X) + halfImageW
spriteRender3DVerts[1].DstY = float32(px.Y) - halfImageH
spriteRender3DVerts[1].SrcX = imageW
spriteRender3DVerts[2].DstX = float32(px.X) + halfImageW
spriteRender3DVerts[2].DstY = float32(px.Y) + halfImageH
spriteRender3DVerts[2].SrcX = imageW
spriteRender3DVerts[2].SrcY = imageH
spriteRender3DVerts[3].DstX = float32(px.X) - halfImageW
spriteRender3DVerts[3].DstY = float32(px.Y) + halfImageH
spriteRender3DVerts[3].SrcY = imageH
shaderOptions := &ebiten.DrawTrianglesShaderOptions{}
shaderOptions.Images[0] = rs.Image
shaderOptions.Images[1] = camera.resultDepthTexture
shaderOptions.Uniforms = map[string]interface{}{
"SpriteDepth": depth,
}
screen.DrawRectShader(colorTextureW, colorTextureH, camera.sprite3DShader, rectShaderOptions)
screen.DrawTrianglesShader(spriteRender3DVerts, spriteRender3DIndices, camera.sprite3DShader, shaderOptions)
}

View File

@ -103,13 +103,11 @@ func (g *Game) Draw(screen *ebiten.Image) {
// Render the scene.
g.Camera.RenderScene(g.Scene)
opt := &ebiten.DrawImageOptions{}
opt.GeoM.Translate(-float64(g.HeartSprite.Bounds().Dx())/2, -float64(g.HeartSprite.Bounds().Dy())/2)
g.Camera.DrawImageIn3D(
// Draw the sprite after the rest of the scene.
g.Camera.DrawSprite3D(
g.Camera.ColorTexture(),
tetra3d.SpriteRender3d{
tetra3d.DrawSprite3dSettings{
Image: g.HeartSprite,
Options: opt,
WorldPosition: g.Scene.Root.Get("Heart").WorldPosition(),
},
)
@ -118,7 +116,7 @@ func (g *Game) Draw(screen *ebiten.Image) {
screen.DrawImage(g.Camera.ColorTexture(), nil)
if g.System.DrawDebugText {
g.Camera.DebugDrawText(screen, "This is an example showing\nhow you can render a sprite in 2D, while\nmaintaining its ability to render over or under\nother 3D objects by simply moving\nit through 3D space.\n\nA: Toggle wireframe view of heart position", 0, 200, 1, colors.LightGray())
g.Camera.DebugDrawText(screen, "This is an example showing\nhow you can render a sprite in 2D, while\nmaintaining its ability to render over or under\nother 3D objects.\n\nA: Toggle wireframe view of heart position", 0, 200, 1, colors.LightGray())
}
if g.WireframeDrawHeart {

View File

@ -36,14 +36,18 @@ func (g *Game) Init() {
mesh := tetra3d.NewCubeMesh()
// Here we specify a fragment shader
// Here we specify a fragment shader...
_, err := mesh.MeshParts[0].Material.SetShaderText([]byte(`
package main
//kage:unit pixels
package main
func Fragment(position vec4, texCoord vec2, color vec4) vec4 {
scrSize := imageDstTextureSize()
return vec4(position.x / scrSize.x, position.y / scrSize.y, 0, 1)
}`))
}`),
)
if err != nil {
panic(err)

12
go.mod
View File

@ -3,18 +3,18 @@ module github.com/solarlune/tetra3d
go 1.18
require (
github.com/hajimehoshi/ebiten/v2 v2.6.1
github.com/hajimehoshi/ebiten/v2 v2.6.2
github.com/qmuntal/gltf v0.24.2
golang.org/x/image v0.12.0
golang.org/x/image v0.13.0
)
require (
github.com/ebitengine/purego v0.5.0 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect
github.com/jezek/xgb v1.1.0 // indirect
golang.org/x/exp/shiny v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/exp/shiny v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/mobile v0.0.0-20231006135142-2b44d11868fe // indirect
golang.org/x/sync v0.4.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
)

12
go.sum
View File

@ -11,6 +11,8 @@ github.com/hajimehoshi/ebiten/v2 v2.5.0 h1:jnz5dngMflIbsIZoj19Vs4zF3kDv1hPUFSeu4
github.com/hajimehoshi/ebiten/v2 v2.5.0/go.mod h1:mnHSOVysTr/nUZrN1lBTRqhK4NG+T9NR3JsJP2rCppk=
github.com/hajimehoshi/ebiten/v2 v2.6.1 h1:ljYS8zp6jp9lFd0zR/2vsIK4Km90/WvTEmxDGdAK8kE=
github.com/hajimehoshi/ebiten/v2 v2.6.1/go.mod h1:TZtorL713an00UW4LyvMeKD8uXWnuIuCPtlH11b0pgI=
github.com/hajimehoshi/ebiten/v2 v2.6.2 h1:tVa3ZJbp4Uz/VSjmpgtQIOvwd7aQH290XehHBLr2iWk=
github.com/hajimehoshi/ebiten/v2 v2.6.2/go.mod h1:TZtorL713an00UW4LyvMeKD8uXWnuIuCPtlH11b0pgI=
github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk=
github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/qmuntal/gltf v0.23.1 h1:R8vkbJXmARbD/oI+Yn3252I2qDQ8mljsc88BJJEdYMY=
@ -24,14 +26,20 @@ golang.org/x/exp/shiny v0.0.0-20230321023759-10a507213a29 h1:uM92tP2dJQAC0zcyUIR
golang.org/x/exp/shiny v0.0.0-20230321023759-10a507213a29/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
golang.org/x/exp/shiny v0.0.0-20230905200255-921286631fa9 h1:rvxT0xShhCtCvCCmF3wMK57nkbTYSaf/0Tp7TAllhMs=
golang.org/x/exp/shiny v0.0.0-20230905200255-921286631fa9/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
golang.org/x/exp/shiny v0.0.0-20231006140011-7918f672742d h1:grE48C8cjIY0aiHVmFyYgYxxSARQWBABLXKZfQPrBhY=
golang.org/x/exp/shiny v0.0.0-20231006140011-7918f672742d/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c h1:Gk61ECugwEHL6IiyyNLXNzmu8XslmRP2dS0xjIYhbb4=
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57 h1:Q6NT8ckDYNcwmi/bmxe+XbiDMXqMRW1xFBtJ+bIpie4=
golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57/go.mod h1:wEyOn6VvNW7tcf+bW/wBz1sehi2s2BZ4TimyR7qZen4=
golang.org/x/mobile v0.0.0-20231006135142-2b44d11868fe h1:lrXv4yHeD9FA8PSJATWowP1QvexpyAPWmPia+Kbzql8=
golang.org/x/mobile v0.0.0-20231006135142-2b44d11868fe/go.mod h1:BrnXpEObnFxpaT75Jo9hsCazwOWcp7nVIa8NNuH5cuA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -44,6 +52,8 @@ golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -54,6 +64,8 @@ golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=

View File

@ -46,7 +46,7 @@ type Material struct {
Texture *ebiten.Image // The texture applied to the Material.
TexturePath string // The path to the texture, if it was not packed into the exporter.
TextureFilterMode ebiten.Filter // Texture filtering mode
TextureWrapMode ebiten.Address // Texture wrapping mode
textureWrapMode ebiten.Address // Texture wrapping mode; this is ignored currently, as all triangles render through shaders, where looping is enforced.
properties Properties // Properties allows you to specify auxiliary data on the Material. This is loaded from GLTF files or Blender's Custom Properties if the setting is enabled on the export menu.
BackfaceCulling bool // If backface culling is enabled (which it is by default), faces turned away from the camera aren't rendered.
TriangleSortMode int // TriangleSortMode influences how triangles with this Material are sorted.
@ -60,8 +60,12 @@ type Material struct {
fragmentShader *ebiten.Shader
// FragmentShaderOn is an easy boolean toggle to control whether the shader is activated or not (it defaults to on).
FragmentShaderOn bool
// FragmentShaderOptions allows you to customize the custom fragment shader with uniforms or images. It does NOT take the
// CompositeMode property from the Material's CompositeMode. By default, it's an empty DrawTrianglesShaderOptions struct.
// FragmentShaderOptions allows you to customize the custom fragment shader with uniforms or images.
// By default, it's an empty DrawTrianglesShaderOptions struct.
// Note that the second image slot is reserved for a depth texture (primarily the texture used to "cut" a
// rendered model); the first image slot by default contains the Texture used in this Material.
// If you want a custom fragment shader that already has fog and depth-testing, use Extend3DBaseShader() to
// extend your custom fragment shader from Tetra3D's base 3D shader.
FragmentShaderOptions *ebiten.DrawTrianglesShaderOptions
fragmentSrc []byte
@ -89,7 +93,7 @@ func NewMaterial(name string) *Material {
Color: NewColor(1, 1, 1, 1),
properties: NewProperties(),
TextureFilterMode: ebiten.FilterNearest,
TextureWrapMode: ebiten.AddressRepeat,
textureWrapMode: ebiten.AddressRepeat,
BackfaceCulling: true,
TriangleSortMode: TriangleSortModeBackToFront,
TransparencyMode: TransparencyModeAuto,
@ -112,7 +116,7 @@ func (material *Material) Clone() *Material {
newMat.Fogless = material.Fogless
newMat.TransparencyMode = material.TransparencyMode
newMat.TextureFilterMode = material.TextureFilterMode
newMat.TextureWrapMode = material.TextureWrapMode
newMat.textureWrapMode = material.textureWrapMode
newMat.CompositeMode = material.CompositeMode
newMat.BillboardMode = material.BillboardMode
@ -136,6 +140,7 @@ func (material *Material) Clone() *Material {
// compositing the finished render to the screen after fog. If the shader is nil, the Material will render using the default Tetra3D
// render setup (e.g. texture, UV values, vertex colors, and vertex lighting).
// SetShader will return the Shader, and an error if the Shader failed to compile.
// Note that custom shaders require usage of pixel-unit Kage shaders.
func (material *Material) SetShaderText(src []byte) (*ebiten.Shader, error) {
if src == nil {
@ -156,7 +161,11 @@ func (material *Material) SetShaderText(src []byte) (*ebiten.Shader, error) {
}
// SetShader sets an already-compiled Kage shader to the Material.
// SetShader sets an already-compiled custom Kage shader to the Material.
// By default, a custom shader will render on top of everything and with no fog.
// Lighting will also be missing, but that's included in the Model's vertex color.
// If you want to extend the base 3D shader, use tetra3d.ExtendBase3DShader().
// Note that custom shaders require usage of pixel-unit Kage shaders.
func (material *Material) SetShader(shader *ebiten.Shader) {
if material.fragmentShader != shader {
material.fragmentShader = shader

View File

@ -1,3 +1,4 @@
//kage:unit pixels
package main
var BGColor vec4
@ -13,9 +14,17 @@ var OutlineThickness float
var OutlineRounded float
var OutlineColor vec4
func Fragment(pos vec4, tex vec2, col vec4) vec4 {
res := imageSrc0UnsafeAt(tex)
imageSize := imageSrcTextureSize()
// This is just a stub to get the shader to compile; we're piggybacking off of
// Tetra3d's default shader, which has depth testing and fog and stuff, so we
// just create a custom fragment shader named CustomFragment.
// func Fragment(dstPos vec4, srcPos vec2, col vec4) vec4 {}
// The CustomFragment function is the same as the default Fragment function
// (and the arguments mean the same things) except for the defaultColor argument,
// which is the result of the shader from the generic 3D shader part.
func CustomFragment(dstPos vec4, srcPos vec2, col vec4) vec4 {
res := imageSrc2UnsafeAt(srcPos)
color := FGColor
transparency := res.a
colorSet := false
@ -26,8 +35,8 @@ func Fragment(pos vec4, tex vec2, col vec4) vec4 {
if ShadowLength > 0.0 && (ShadowVector.x != 0 || ShadowVector.y != 0) && res.a < 0.5 {
shadowSet = true
shadowV := vec2(ShadowVector.x / imageSize.x, ShadowVector.y / imageSize.y)
check := tex
shadowV := vec2(ShadowVector.x, ShadowVector.y)
check := srcPos
for i := 1.0; i < 16.0; i++ {
@ -39,20 +48,20 @@ func Fragment(pos vec4, tex vec2, col vec4) vec4 {
check.x += shadowV.x
if imageSrc0At(check).a > 0.5 {
if imageSrc2UnsafeAt(check).a > 0.5 {
shaded = true
}
check.y += shadowV.y
if imageSrc0At(check).a > 0.5 {
if imageSrc2UnsafeAt(check).a > 0.5 {
shaded = true
}
if shaded {
color = ShadowColorNear
if ShadowColorFarSet > 0 {
color = mix(ShadowColorNear, ShadowColorFar, (distance(check * imageSize, tex * imageSize)-1) / ShadowLength)
color = mix(ShadowColorNear, ShadowColorFar, (distance(check, srcPos)-1) / ShadowLength)
}
transparency = 1.0
colorSet = true
@ -81,14 +90,14 @@ func Fragment(pos vec4, tex vec2, col vec4) vec4 {
if x >= -OutlineThickness && x <= OutlineThickness && y >= -OutlineThickness && y <= OutlineThickness {
srcTexels := vec2(tex.x + ((x + 0.01) / imageSize.x), tex.y + ((y + 0.01) / imageSize.y))
srcTexels := vec2(srcPos.x + (x + 0.01), srcPos.y + (y + 0.01))
if shadowSet {
srcTexels += (ShadowVector / imageSize) * shadowI
srcTexels += ShadowVector * shadowI
}
if imageSrc0At(srcTexels).a > 0.5 {
if imageSrc2UnsafeAt(srcTexels).a > 0.5 {
if OutlineRounded < 1 || (distance((tex*imageSize) + vec2(x,y), tex*imageSize) <= OutlineThickness) {
if OutlineRounded < 1 || (distance(srcPos + vec2(x,y), srcPos) <= OutlineThickness) {
color = OutlineColor
transparency = 1.0
@ -116,18 +125,8 @@ func Fragment(pos vec4, tex vec2, col vec4) vec4 {
}
// Alternate between the BG color and the FG color using the transparency of the original text image
// as modulation
return mix(BGColor.rgba * col, color * col, transparency)
// return vec4(0.5, 0.0, 0.0, 1.0)
}
// func Fragment(pos vec4, tex vec2, col vec4) vec4 {
// v := 1
// if v > 0 {
// }
// return vec4(1, 0, 0, 1)
// }

View File

@ -116,11 +116,14 @@ func NewText(meshPart *MeshPart, textureWidth int) *Text {
// We set this because Alpha Clip doesn't work with shadows / outlines, as just the text itself writes depth values
meshPart.Material.TransparencyMode = TransparencyModeTransparent
_, err := meshPart.Material.SetShaderText(textShaderSrc)
// shader, err := ebiten.NewShader(textShaderSrc)
shader, err := ExtendBase3DShader(string(textShaderSrc))
if err != nil {
panic(err)
}
meshPart.Material.SetShader(shader)
// We set the default text here so that something appears, and we
// apply a style using the function because otherwise, the text would be invisible.
text.setText = "Default text"

188
utils.go
View File

@ -3,7 +3,9 @@ package tetra3d
import (
"image"
"math"
"strings"
"github.com/hajimehoshi/ebiten/v2"
"golang.org/x/image/font"
)
@ -81,3 +83,189 @@ func measureText(text string, fontFace font.Face) image.Rectangle {
// newBounds := TextMeasure{int((bounds.Max.X - bounds.Min.X) >> 6), int((bounds.Max.Y - bounds.Min.Y) >> 6)}
return newBounds
}
// ExtendBase3DShader allows you to make a custom fragment shader that extends the base 3D shader, allowing you
// to make a shader that has fog and depth testing built-in, as well as access to the combined painted vertex
// colors and vertex-based lighting.
// To do this, make a Kage shader, but rename the fragment entry point from "Fragment()" to "CustomFragment()".
// Otherwise, the arguments are the same - dstPos, srcPos, and color, with color containing both vertex color
// and lighting data. The return vec4 is also the same - the value that is returned from CustomFragment will be
// used for fog.
// To turn off lighting or fog individually, you would simply turn on shadelessness and foglessness in
// your object's Material (or shadelessness in your Model itself).
func ExtendBase3DShader(customFragment string) (*ebiten.Shader, error) {
shaderText := `
//kage:unit pixels
package main
var Fog vec4
var FogRange [2]float
var DitherSize float
var FogCurve float
var Fogless float
var BayerMatrix [16]float
func decodeDepth(rgba vec4) float {
return rgba.r + (rgba.g / 255) + (rgba.b / 65025)
}
func OutCirc(v float) float {
return sqrt(1 - pow(v - 1, 2))
}
func InCirc(v float) float {
return 1 - sqrt(1 - pow(v, 2))
}
func dstPosToSrcPos(dstPos vec2) vec2 {
return dstPos.xy - imageDstOrigin() + imageSrc0Origin()
}
// tetra3d Custom Uniform Location //
func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
depth := imageSrc1UnsafeAt(dstPosToSrcPos(dstPos.xy))
if depth.a > 0 {
srcOrigin := imageSrc0Origin()
srcSize := imageSrc0Size()
// There's atlassing going on behind the scenes here, so:
// Subtract the source position by the src texture's origin on the atlas.
// This gives us the actual pixel coordinates.
tx := srcPos - srcOrigin
// Divide by the source image size to get the UV coordinates.
tx /= srcSize
// Apply fract() to loop the UV coords around [0-1].
tx = fract(tx)
// Multiply by the size to get the pixel coordinates again.
tx *= srcSize
// tetra3d Custom Fragment Call Location //
colorTex := imageSrc0UnsafeAt(tx + srcOrigin) * color
if Fogless == 0 {
var d float
if FogCurve == 0 {
d = smoothstep(FogRange[0], FogRange[1], decodeDepth(depth))
} else if FogCurve == 1 {
d = smoothstep(FogRange[0], FogRange[1], OutCirc(decodeDepth(depth)))
} else if FogCurve == 2 {
d = smoothstep(FogRange[0], FogRange[1], InCirc(decodeDepth(depth)))
}
if DitherSize > 0 {
yc := int(dstPos.y / DitherSize)%4
xc := int(dstPos.x / DitherSize)%4
fogMult := step(0, d - BayerMatrix[(yc*4) + xc])
// Fog mode is 4th channel in Fog vector ([4]float32)
if Fog.a == 0 {
colorTex.rgb += Fog.rgb * fogMult * colorTex.a
} else if Fog.a == 1 {
colorTex.rgb -= Fog.rgb * fogMult * colorTex.a
} else if Fog.a == 2 {
colorTex.rgb = mix(colorTex.rgb, Fog.rgb, fogMult) * colorTex.a
} else if Fog.a == 3 {
colorTex.a *= abs(1-d) * step(0, abs(1-d) - BayerMatrix[(yc*4) + xc])
colorTex.rgb = mix(vec3(0, 0, 0), colorTex.rgb, colorTex.a)
}
} else {
if Fog.a == 0 {
colorTex.rgb += Fog.rgb * d * colorTex.a
} else if Fog.a == 1 {
colorTex.rgb -= Fog.rgb * d * colorTex.a
} else if Fog.a == 2 {
colorTex.rgb = mix(colorTex.rgb, Fog.rgb, d) * colorTex.a
} else if Fog.a == 3 {
colorTex.a *= abs(1-d)
colorTex.rgb = mix(vec3(0, 0, 0), colorTex.rgb, colorTex.a)
}
}
}
return colorTex
}
discard()
}
// tetra3d Custom Fragment Definition Location //
`
if len(customFragment) > 0 {
shaderTextSplit := strings.Split(shaderText, "\n")
customTextSplit := strings.Split(customFragment, "\n")
uniformLocationDest := -1
customFragmentCallLocation := -1
customFragmentDefinitionLocation := -1
for i, line := range shaderTextSplit {
if strings.Contains(line, "// tetra3d Custom Uniform Location //") {
uniformLocationDest = i
}
if strings.Contains(line, "// tetra3d Custom Fragment Call Location //") {
customFragmentCallLocation = i + 1
}
if strings.Contains(line, "// tetra3d Custom Fragment Definition Location //") {
customFragmentDefinitionLocation = i
}
}
customShaderStart := -1
fragFunctionStart := -1
for i, line := range customTextSplit {
if strings.Contains(line, "package main") {
customShaderStart = i + 1
}
if strings.Contains(line, "func Fragment(") {
fragFunctionStart = i
}
}
out := ""
for i, line := range shaderTextSplit {
if i == uniformLocationDest {
out += strings.Join(customTextSplit[customShaderStart:fragFunctionStart], "\n") + "\n"
out += line + "\n"
} else if i == customFragmentCallLocation {
// Replace teh line with a new ColorTex definition
out += "colorTex := CustomFragment(dstPos, srcPos, color)\n\n"
} else if i == customFragmentDefinitionLocation {
out += strings.Join(customTextSplit[fragFunctionStart:], "\n") + "\n"
out += line + "\n"
} else {
out += line + "\n"
}
}
shaderText = out
}
return ebiten.NewShader([]byte(shaderText))
}