diff --git a/AGENTS.md b/AGENTS.md
index 4e0b4f57f..4dfba43d9 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -78,6 +78,13 @@ kaiju/
└── AGENTS.md # This file
```
+## UI & Engine Interoperability Guidelines (CRITICAL)
+
+When creating or modifying User Interfaces (HUDs, Menus, Overlays) within Kaiju Engine, AI agents must strict adhere to the following axioms:
+1. **Always preserve HTML/CSS layout sovereignty.** The document parser (`markup.DocumentFromHTMLAsset`) uses the bounding boundaries defined in the CSS (`position`, `top`, `width`, `height`) to anchor UI meshes correctly to the screen canvas viewport. DO NOT strip structural styling from HTML placeholders to reconstruct bounding boxes exclusively via Go transforms (`Transform.SetLocalPosition`). Removing these HTML anchors will cause render meshes to collapse to `(0,0)` or visually disappear.
+2. **Mandatory Explicit Dirty Flags**: Kaiju UI rendering does not redraw structural changes automatically via setters in the loop. If you hook into an element via Go and modify its inner text (`label.SetText()`) or dynamic coloring, you MUST explicitly tell the GPU pipeline to redraw that interface by firing `element.UI.SetDirty(ui.DirtyTypeLayout)` before the frame ends.
+3. **Hierarchical Panels vs Labels Nature**: A standard block like `
Text
` implicitly generates a `UIPanel` wrapper hosting a pure `UILabel` child. Background colors must be painted strictly on the outer Panel (`UIPanel.SetBGColor`), while internal Font styling applies exclusively to the `InnerLabel()`. Mixing these commands results in UI rendering glitches.
+
## CRITICAL: Custom Math Library
**DO NOT use external math libraries (e.g., gonum, mathgl).** The Kaiju Engine has a complete custom math library at `kaijuengine.com/matrix`.
diff --git a/docs/engine/ui/writing.md b/docs/engine/ui/writing.md
index 654ec3a57..248dbdea2 100644
--- a/docs/engine/ui/writing.md
+++ b/docs/engine/ui/writing.md
@@ -72,4 +72,36 @@ data := struct{
doc, err := markup.DocumentFromHTMLAsset(host, "ui/tests/binding.html", data, nil)
```
-This will load up the HTML document and any of the CSS it references and build out your UI. The returned `doc` will contain the document and all the elements/panels/labels. This UI is immediately loaded into the `host` so you don't need to worry about doing that yourself. *The last argument is a funcmap used for inline template functions*
\ No newline at end of file
+This will load up the HTML document and any of the CSS it references and build out your UI. The returned `doc` will contain the document and all the elements/panels/labels. This UI is immediately loaded into the `host` so you don't need to worry about doing that yourself. *The last argument is a funcmap used for inline template functions*
+## Dynamic UI Updates (Go Interoperability)
+While HTML/CSS provides the initial structural foundation and styling (the canvas initial geometric bounding box), you will commonly need to update the UI dynamically during runtime from your game's Go logic (e.g., updating a timer, health bar, or coordinate tracker).
+
+### 1. Element Hierarchy (Panel vs Label)
+When declaring a simple markup with text in HTML:
+`html
+FPS: --
+`
+The parsing engine instantiates a ui.UIPanel base structural wrapper, and creates an attached hierarchical child ui.UILabel for the string text logic.
+To retrieve and modify properties dynamically in the Go loop:
+* **Backgrounds & Solid Borders**: Are applied to the parent pane (element.UIPanel.SetBGColor(...)).
+* **Text Strings & Typography Colors**: Are applied to the inner textual label (element.InnerLabel().SetText(...) or element.InnerLabel().SetColor(...)).
+
+### 2. Dirty Flags (Reactivity is NOT Automatic)
+Unlike standard web browsers, the Kaiju Engine does NOT automatically redraw the UI canvas frame when you modify an element's property via backend Go scripts. You **must explicitly** flag the element as "dirty" in your game or system's update loop to force the Vulkan render pipeline to redraw the updated geometry on the screen:
+`go
+if e, ok := doc.GetElementById("fps-val"); ok {
+ if lbl := e.InnerLabel(); lbl != nil {
+ lbl.SetText(fmt.Sprintf("FPS: %d", fps))
+ }
+ // Critical: Signal the renderer that this specific element layout has mutated!
+ e.UI.SetDirty(ui.DirtyTypeLayout)
+}
+`
+
+### 3. Layout Control Pitfalls (The Invisible Canvas Bug)
+**Never strip foundational HTML positioning anchors in an attempt to control the Screen Layout purely via Go Transform functions.**
+
+The document parser deeply relies on CSS properties (position: absolute, op, ottom,
ight, width, height) to anchor the UI primitives to the Screen Viewport properly.
+
+If you declare empty/naked tags in the HTML and attempt to explicitly align and mathematically scale them exclusively via Transform.SetLocalPosition(...) or manual offset matrices natively in Go, the engine's internal UI-Layout System will fight the DOM logic. This typically cascades in elements infinitely collapsing to coordinates (0,0), or losing their mesh dimension entirely, making your UI visually disappear from the screen logic.
+* **Golden Rule**: HTML/CSS solely owns the global layout spatial anchors, responsivity bounds, and z-index ordering. Go owns dynamic text injection, loop data binding, and situational state-color updates.
diff --git a/src/config.json b/src/config.json
new file mode 100644
index 000000000..04d5e1dd8
--- /dev/null
+++ b/src/config.json
@@ -0,0 +1,23 @@
+{
+ "window_width": 1280,
+ "window_height": 720,
+ "window_title": "FortressVision",
+ "fullscreen": false,
+ "target_fps": 60,
+ "dfhack_host": "localhost",
+ "dfhack_port": 5000,
+ "server_url": "ws://127.0.0.1:8080/ws",
+ "draw_distance": 10,
+ "view_levels": 5,
+ "mesher_threads": 4,
+ "fov": 60,
+ "draw_range_down": 5,
+ "draw_range_up": 1,
+ "draw_range_side": 4,
+ "camera_speed": 10,
+ "camera_sensitivity": 0.3,
+ "zoom_speed": 5,
+ "show_debug_info": true,
+ "show_grid": false,
+ "wireframe_mode": false
+}
\ No newline at end of file
diff --git a/src/editor/editor_embedded_content/editor_content/renderer/spv/ui_nine.frag.spv b/src/editor/editor_embedded_content/editor_content/renderer/spv/ui_nine.frag.spv
index b47162aea..15ee140a4 100644
Binary files a/src/editor/editor_embedded_content/editor_content/renderer/spv/ui_nine.frag.spv and b/src/editor/editor_embedded_content/editor_content/renderer/spv/ui_nine.frag.spv differ
diff --git a/src/editor/editor_embedded_content/editor_content/renderer/spv/ui_transparent_ui_nine.frag.spv b/src/editor/editor_embedded_content/editor_content/renderer/spv/ui_transparent_ui_nine.frag.spv
index 947551ce5..20233c6ef 100644
Binary files a/src/editor/editor_embedded_content/editor_content/renderer/spv/ui_transparent_ui_nine.frag.spv and b/src/editor/editor_embedded_content/editor_content/renderer/spv/ui_transparent_ui_nine.frag.spv differ
diff --git a/src/editor/editor_embedded_content/editor_content/renderer/src/inc_fragment_oit_block.inl b/src/editor/editor_embedded_content/editor_content/renderer/src/inc_fragment_oit_block.inl
index 69cca32bf..738321923 100644
--- a/src/editor/editor_embedded_content/editor_content/renderer/src/inc_fragment_oit_block.inl
+++ b/src/editor/editor_embedded_content/editor_content/renderer/src/inc_fragment_oit_block.inl
@@ -6,8 +6,5 @@
float weight = clamp(pow(min(1.0, unWeightedColor.a * 10.0) + 0.01, 3.0) * 1e8 * pow(1.0 - gl_FragCoord.z * 0.9, 3.0), 1e-2, 3e3);
outColor = vec4(unWeightedColor.rgb * unWeightedColor.a, unWeightedColor.a) * weight;
#else
- if (unWeightedColor.a < (1.0 - 0.001)) {
- discard;
- }
outColor = unWeightedColor;
#endif
\ No newline at end of file
diff --git a/src/editor/editor_embedded_content/editor_content/renderer/src/ui_nine.frag b/src/editor/editor_embedded_content/editor_content/renderer/src/ui_nine.frag
index eac98e7d1..ec3324d72 100644
--- a/src/editor/editor_embedded_content/editor_content/renderer/src/ui_nine.frag
+++ b/src/editor/editor_embedded_content/editor_content/renderer/src/ui_nine.frag
@@ -47,7 +47,10 @@ void main(void) {
processAxis(normUV.y, fragNineSliceEdgeLen.y / fragSize2D.w, fragSize2D.w / fragSize2D.y)
);
vec2 newUV = fragUvs.xy + scaledNormUV * fragUvs.zw;
- vec4 unWeightedColor = texture(texSampler, newUV) * fragColor;
+ vec4 texColor = texture(texSampler, newUV);
+ // Lógica Premium de Cor de Fundo:
+ // Misturamos fragBGColor (fundo sólido) com (texColor * fragColor) baseado no alpha da textura.
+ vec4 unWeightedColor = mix(fragBGColor, texColor * fragColor, texColor.a);
// Border
{
vec2 dimensions = fragSize2D.xy;
diff --git a/src/engine/entity.go b/src/engine/entity.go
index 9a971ed37..251f1d8eb 100644
--- a/src/engine/entity.go
+++ b/src/engine/entity.go
@@ -87,7 +87,9 @@ func NewEntity(workGroup *concurrent.WorkGroup) *Entity {
func (e *Entity) Init(workGroup *concurrent.WorkGroup) {
e.isActive = true
e.Children = make([]*Entity, 0)
- e.Transform.Initialize(workGroup)
+ if e != nil {
+ e.Transform.Initialize(workGroup)
+ }
e.namedData = make(map[string][]interface{})
e.name = "Entity"
}
diff --git a/src/engine/ui/markup/css/properties/css_background_color.go b/src/engine/ui/markup/css/properties/css_background_color.go
index 134874404..1644b871a 100644
--- a/src/engine/ui/markup/css/properties/css_background_color.go
+++ b/src/engine/ui/markup/css/properties/css_background_color.go
@@ -46,6 +46,7 @@ import (
"kaijuengine.com/engine/ui/markup/css/rules"
"kaijuengine.com/engine/ui/markup/document"
"kaijuengine.com/matrix"
+ "kaijuengine.com/rendering"
)
func setChildTextBackgroundColor(elm *document.Element, color matrix.Color) {
@@ -91,7 +92,10 @@ func (p BackgroundColor) Process(panel *ui.Panel, elm *document.Element, values
}
if color, err = matrix.ColorFromHexString(hex); err == nil {
if applyPanelColor || panel.Base().Type() == ui.ElementTypeImage {
+ tex, _ := host.TextureCache().Texture(assets.TextureSquare, rendering.TextureFilterLinear)
+ panel.SetBackground(tex)
panel.SetColor(color)
+ panel.SetBGColor(color)
}
if panel.Base().IsType(ui.ElementTypeInput) {
panel.Base().ToInput().SetBGColor(color)
diff --git a/src/engine/ui/markup/css/properties/css_font_family.go b/src/engine/ui/markup/css/properties/css_font_family.go
index c5d9ba696..c36aca832 100644
--- a/src/engine/ui/markup/css/properties/css_font_family.go
+++ b/src/engine/ui/markup/css/properties/css_font_family.go
@@ -50,9 +50,14 @@ import (
func setChildrenFontFace(elm *document.Element, face rendering.FontFace) {
defer tracing.NewRegion("properties.setChildrenFontFace").End()
+ if elm.UI == nil {
+ return
+ }
if elm.IsText() {
lbl := elm.UI.ToLabel()
- lbl.SetFontFace(face)
+ if lbl != nil {
+ lbl.SetFontFace(face)
+ }
} else if elm.UI.IsType(ui.ElementTypeInput) {
elm.UI.ToInput().SetFontFace(face)
} else {
diff --git a/src/engine/ui/markup/css/properties/css_font_size.go b/src/engine/ui/markup/css/properties/css_font_size.go
index 65b4a506d..c7f971309 100644
--- a/src/engine/ui/markup/css/properties/css_font_size.go
+++ b/src/engine/ui/markup/css/properties/css_font_size.go
@@ -46,14 +46,16 @@ import (
)
func setChildrenFontSize(elm *document.Element, size string, host *engine.Host) {
- if elm.Stylizer.HasRule("font-size") {
+ if elm.Stylizer.HasRule("font-size") || elm.UI == nil {
return
}
if elm.IsText() {
lbl := elm.UI.ToLabel()
- size := helpers.NumFromLengthWithFont(size, host.Window,
- host.FontCache().EMSize(lbl.FontFace()))
- lbl.SetFontSize(size)
+ if lbl != nil {
+ size := helpers.NumFromLengthWithFont(size, host.Window,
+ host.FontCache().EMSize(lbl.FontFace()))
+ lbl.SetFontSize(size)
+ }
} else {
for _, child := range elm.Children {
setChildrenFontSize(child, size, host)
diff --git a/src/engine/ui/markup/css/properties/css_font_style.go b/src/engine/ui/markup/css/properties/css_font_style.go
index 42ec930cd..545c644e9 100644
--- a/src/engine/ui/markup/css/properties/css_font_style.go
+++ b/src/engine/ui/markup/css/properties/css_font_style.go
@@ -45,11 +45,16 @@ import (
)
func setChildrenFontStyle(elm *document.Element, style string) {
+ if elm.UI == nil {
+ return
+ }
if elm.IsText() {
lbl := elm.UI.ToLabel()
- lbl.SetFontStyle(style)
+ if lbl != nil {
+ lbl.SetFontStyle(style)
+ }
} else if elm.UI.IsType(ui.ElementTypeInput) {
- elm.UI.ToInput().SetFontWeight(style)
+ elm.UI.ToInput().SetFontStyle(style)
} else {
for _, child := range elm.Children {
setChildrenFontStyle(child, style)
diff --git a/src/engine/ui/markup/css/properties/css_font_weight.go b/src/engine/ui/markup/css/properties/css_font_weight.go
index ecaca13a5..32897a9f0 100644
--- a/src/engine/ui/markup/css/properties/css_font_weight.go
+++ b/src/engine/ui/markup/css/properties/css_font_weight.go
@@ -45,9 +45,14 @@ import (
)
func setChildrenFontWeight(elm *document.Element, weight string) {
+ if elm.UI == nil {
+ return
+ }
if elm.IsText() {
lbl := elm.UI.ToLabel()
- lbl.SetFontWeight(weight)
+ if lbl != nil {
+ lbl.SetFontWeight(weight)
+ }
} else if elm.UI.IsType(ui.ElementTypeInput) {
elm.UI.ToInput().SetFontWeight(weight)
} else {
diff --git a/src/engine/ui/markup/css/properties/css_left.go b/src/engine/ui/markup/css/properties/css_left.go
index 761871a3b..0fb92927f 100644
--- a/src/engine/ui/markup/css/properties/css_left.go
+++ b/src/engine/ui/markup/css/properties/css_left.go
@@ -40,6 +40,7 @@ import (
"errors"
"kaijuengine.com/engine"
"kaijuengine.com/engine/ui"
+ "kaijuengine.com/engine/ui/markup/css/helpers"
"kaijuengine.com/engine/ui/markup/css/rules"
"kaijuengine.com/engine/ui/markup/document"
"strings"
@@ -63,7 +64,7 @@ func (p Left) Process(panel *ui.Panel, elm *document.Element, values []rules.Pro
offsetX += elm.Parent.Value().UI.Layout().Offset().X()
}
default:
- val := values[0].Num
+ val := helpers.NumFromLength(values[0].Str, host.Window)
if strings.HasSuffix(values[0].Str, "%") {
l := panel.Base().Layout()
if l.Ui().Entity().IsRoot() {
diff --git a/src/engine/ui/markup/css/properties/css_line_height.go b/src/engine/ui/markup/css/properties/css_line_height.go
index 3d410a99a..a6a8597f7 100644
--- a/src/engine/ui/markup/css/properties/css_line_height.go
+++ b/src/engine/ui/markup/css/properties/css_line_height.go
@@ -46,15 +46,17 @@ import (
)
func setChildrenLineHeight(elm *document.Element, size string, host *engine.Host) {
- if elm.Stylizer.HasRule("line-height") {
+ if elm.Stylizer.HasRule("line-height") || elm.UI == nil {
return
}
if elm.IsText() {
lbl := elm.UI.ToLabel()
- height := helpers.NumFromLengthWithFont(size, host.Window,
- host.FontCache().EMSize(lbl.FontFace()))
- height = elm.Parent.Value().UI.Layout().PixelSize().Y() * height
- lbl.SetLineHeight(height)
+ if lbl != nil {
+ size := helpers.NumFromLengthWithFont(size, host.Window,
+ host.FontCache().EMSize(lbl.FontFace()))
+ height := elm.Parent.Value().UI.Layout().PixelSize().Y() * size
+ lbl.SetLineHeight(height)
+ }
} else {
for _, child := range elm.Children {
setChildrenLineHeight(child, size, host)
diff --git a/src/engine/ui/markup/html.go b/src/engine/ui/markup/html.go
index fd1545523..181c1b9b9 100644
--- a/src/engine/ui/markup/html.go
+++ b/src/engine/ui/markup/html.go
@@ -41,6 +41,7 @@ import (
"kaijuengine.com/engine"
"kaijuengine.com/engine/ui"
"kaijuengine.com/engine/ui/markup/css"
+ _ "kaijuengine.com/engine/ui/markup/css/properties"
"kaijuengine.com/engine/ui/markup/css/rules"
"kaijuengine.com/engine/ui/markup/document"
"log/slog"
diff --git a/src/engine/ui/panel.go b/src/engine/ui/panel.go
index 213fb6e4b..c2c8dcad9 100644
--- a/src/engine/ui/panel.go
+++ b/src/engine/ui/panel.go
@@ -600,12 +600,17 @@ func (p *Panel) UnEnforceColor() {
func (p *Panel) Color() matrix.Color { return p.shaderData.FgColor }
-func (p *Panel) SetColor(bgColor matrix.Color) {
+func (p *Panel) SetColor(color matrix.Color) {
if p.HasEnforcedColor() {
- p.PanelData().enforcedColorStack[0] = bgColor
+ p.PanelData().enforcedColorStack[0] = color
return
}
- p.setColorInternal(bgColor)
+ p.setColorInternal(color)
+}
+
+func (p *Panel) SetBGColor(color matrix.Color) {
+ p.shaderData.BgColor = color
+ p.Base().SetDirty(DirtyTypeColorChange)
}
func (p *Panel) SetScrollX(value float32) {
diff --git a/src/matrix/transform.go b/src/matrix/transform.go
index 02f2a855f..035f9ae74 100644
--- a/src/matrix/transform.go
+++ b/src/matrix/transform.go
@@ -78,6 +78,9 @@ func (t *Transform) setup() {
}
func (t *Transform) Initialize(workGroup *concurrent.WorkGroup) {
+ if t == nil {
+ return
+ }
defer tracing.NewRegion("matrix.Initialize").End()
t.workGroup = workGroup
t.setup()
diff --git a/src/rendering/font.go b/src/rendering/font.go
index 8c08d3fa1..2af1e8d2e 100644
--- a/src/rendering/font.go
+++ b/src/rendering/font.go
@@ -146,7 +146,7 @@ func (cache *FontCache) TransparentMaterial(target *Material) *Material {
} else if target.SelectRoot() == cache.textOrthoMaterial.SelectRoot() {
return cache.textOrthoMaterialTransparent
} else if target.SelectRoot() == cache.textMaterialTransparent.SelectRoot() ||
- target == cache.textOrthoMaterialTransparent.SelectRoot() {
+ target.SelectRoot() == cache.textOrthoMaterialTransparent.SelectRoot() {
return target
}
slog.Error("invalid material used for getting transparent text material", "material", target.Id)
@@ -265,6 +265,9 @@ func (cache *FontCache) cachedMeshLetter(font fontBin, letter rune, isOrtho bool
func (cache *FontCache) createLetterMesh(font fontBin, key rune, c fontBinChar, meshCache *MeshCache) {
defer tracing.NewRegion("FontCache.createLetterMesh").End()
+ if font.cachedLetters == nil || font.cachedOrthoLetters == nil {
+ return
+ }
mat := cache.textMaterial
oMat := cache.textOrthoMaterial
@@ -307,12 +310,15 @@ func (cache *FontCache) createLetterMesh(font fontBin, key rune, c fontBinChar,
func (cache *FontCache) initFont(face FontFace, adb assets.Database) bool {
defer tracing.NewRegion("FontCache.initFont").End()
bin := fontBin{}
- bin.texture, _ = cache.renderCaches.TextureCache().Texture(face.string()+".png", TextureFilterLinear)
- bin.texture.MipLevels = 1
bin.cachedLetters = make(map[rune]*cachedLetterMesh)
bin.cachedOrthoLetters = make(map[rune]*cachedLetterMesh)
+ bin.texture, _ = cache.renderCaches.TextureCache().Texture(face.string()+".png", TextureFilterLinear)
+ if bin.texture == nil {
+ return false
+ }
+ bin.texture.MipLevels = 1
out, _ := adb.Read(face.string() + ".bin")
- if bin.texture == nil || out == nil || len(out) == 0 {
+ if out == nil || len(out) == 0 {
return false
}
read := bytes.NewReader(out)
@@ -421,6 +427,10 @@ func (cache *FontCache) RenderMeshes(caches RenderCaches,
material = cache.textOrthoMaterial
}
}
+ if material == nil {
+ slog.Error("FontCache.RenderMeshes: material de texto nulo", "transparência", fgColor.A() < 1 || bgColor.A() < 1, "is3D", is3D)
+ return nil
+ }
// Iterate through all characters
runes := []rune(text)
textLen := len(runes)
@@ -548,6 +558,9 @@ func (cache *FontCache) RenderMeshes(caches RenderCaches,
Scissor: matrix.Vec4{-matrix.FloatMax, -matrix.FloatMax, matrix.FloatMax, matrix.FloatMax},
}
shaderData.SetModel(model)
+ if material == nil || fontFace.texture == nil {
+ continue
+ }
drawing := Drawing{
Material: material.CreateInstance([]*Texture{fontFace.texture}),
Mesh: m,
diff --git a/src/rendering/gpu_device_drawing_vulkan.go b/src/rendering/gpu_device_drawing_vulkan.go
index 8e47d0fd9..15ae53c45 100644
--- a/src/rendering/gpu_device_drawing_vulkan.go
+++ b/src/rendering/gpu_device_drawing_vulkan.go
@@ -145,9 +145,17 @@ func (g *GPUDevice) blitTargetsImpl(passes []*RenderPass) {
cmd.Begin()
defer cmd.End()
g.Painter.forceQueueCommand(*cmd, false)
+
frame := g.Painter.currentFrame
idxSF := g.Painter.imageIndex[frame]
swapChain := g.LogicalDevice.SwapChain
+
+ if img == nil {
+ g.TransitionImageLayout(&swapChain.Images[idxSF], GPUImageLayoutPresentSrc,
+ GPUImageAspectColorBit, GPUAccessTransferWriteBit, cmd)
+ return
+ }
+
g.TransitionImageLayout(&swapChain.Images[idxSF],
GPUImageLayoutTransferDstOptimal, GPUImageAspectColorBit,
GPUAccessTransferWriteBit, cmd)
@@ -370,6 +378,9 @@ func (g *GPUDevice) combineTargets() *TextureId {
cmd.Begin()
defer cmd.End()
g.Painter.forceQueueCommand(*cmd, false)
+ if len(g.Painter.combinedDrawings.renderPassGroups) == 0 {
+ return nil
+ }
// There is only one render pass in combined, so we can just grab the first one
draws := g.Painter.combinedDrawings.renderPassGroups[0].draws
for i := range draws[0].instanceGroups {
@@ -386,6 +397,11 @@ func (g *GPUDevice) combineTargets() *TextureId {
func (g *GPUDevice) cleanupCombined(cmd *CommandRecorder) {
defer tracing.NewRegion("Vulkan.cleanupCombined").End()
+
+ if len(g.Painter.combinedDrawings.renderPassGroups) == 0 {
+ return
+ }
+
// There is only one render pass in combined, so we can just grab the first one
groups := g.Painter.combinedDrawings.renderPassGroups[0].draws[0].instanceGroups
for i := range groups {
diff --git a/src/rendering/material.go b/src/rendering/material.go
index 5ba537201..19ba698f3 100644
--- a/src/rendering/material.go
+++ b/src/rendering/material.go
@@ -97,6 +97,10 @@ func (m *Material) CreateInstance(textures []*Texture) *Material {
defer tracing.NewRegion("Material.CreateInstance").End()
instanceKey := strings.Builder{}
for i := range textures {
+ if textures[i] == nil {
+ instanceKey.WriteString("nil;")
+ continue
+ }
instanceKey.WriteString(textures[i].Key)
instanceKey.WriteRune(';')
}