Skip to content

Commit 1a0501e

Browse files
committed
Allow image reuse with different paths
Store image data separately from texture, allowing comparison of image data that is the same even from a different file.
1 parent d30fcee commit 1a0501e

File tree

1 file changed

+84
-28
lines changed

1 file changed

+84
-28
lines changed

Sources/GateEngine/Helpers/TextureAtlas.swift

Lines changed: 84 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,31 @@ public struct TextureAtlas {
7474
extension TextureAtlasBuilder {
7575
struct Texture: Equatable, Hashable, Sendable {
7676
let resourcePath: String
77-
let width: Int
78-
let height: Int
79-
let imageData: Data
80-
let coordinate: (x: Int, y: Int)
81-
77+
var dataIndex: Array<Data>.Index
78+
8279
nonisolated static func == (lhs: Texture, rhs: Texture) -> Bool {
8380
return lhs.resourcePath == rhs.resourcePath
8481
}
8582
nonisolated func hash(into hasher: inout Hasher) {
8683
hasher.combine(resourcePath)
8784
}
8885
}
86+
struct TextureData: Equatable, Hashable, Sendable {
87+
let width: Int
88+
let height: Int
89+
let imageData: Data
90+
var coordinate: (x: Int, y: Int)
91+
92+
nonisolated static func == (lhs: TextureData, rhs: TextureData) -> Bool {
93+
guard lhs.width == rhs.width && lhs.height == rhs.height else {return false}
94+
return lhs.imageData.elementsEqual(rhs.imageData)
95+
}
96+
nonisolated func hash(into hasher: inout Hasher) {
97+
hasher.combine(width)
98+
hasher.combine(height)
99+
hasher.combine(imageData)
100+
}
101+
}
89102

90103
func textureBlocksWide(texturePixelWidth: Float) -> Int {
91104
return Int(ceil(texturePixelWidth / Float(blockSize)))
@@ -98,6 +111,7 @@ extension TextureAtlasBuilder {
98111

99112
public final class TextureAtlasBuilder {
100113
var textures: [TextureAtlasBuilder.Texture] = []
114+
var textureDatas: [TextureAtlasBuilder.TextureData] = []
101115

102116
/// true if this builder has changed since a TextureAtlas was generated
103117
public private(set) var needsGenerate: Bool = true
@@ -110,31 +124,50 @@ public final class TextureAtlasBuilder {
110124
var searchGrid: SearchGrid = SearchGrid()
111125

112126
public init(blockSize: Int) {
113-
assert(blockSize > 0, "blockSize must be positive.")
127+
assert(blockSize >= 0, "blockSize must be positive.")
114128
assert(blockSize <= 1024, "blockSize cannot be greater than 1024.")
115129
self.blockSize = blockSize
116130
}
117131

132+
/// - returns: `true` if the atlas already has the texture in question
118133
public func containsTexture(withPath unresolvedPath: String) -> Bool {
119134
return textures.contains(where: {$0.resourcePath == unresolvedPath})
120135
}
121136

122-
public func insertTexture(withPath unresolvedPath: String) throws {
137+
/**
138+
Adds a new texture, or updates an existing texture.
139+
140+
- parameter unresolvedPath: The resource path to the texture data.
141+
- parameter sacrificePerformanceForSize: When `true` additional checks are performed to merge textures that are the same but with different paths. Resulting in a smaller atlas, at the cost of performance.
142+
*/
143+
public func insertTexture(withPath unresolvedPath: String, sacrificePerformanceForSize: Bool = false) throws {
144+
if containsTexture(withPath: unresolvedPath) {
145+
// Cleanup old values
146+
// If the texture has changed on disk we want to replace it
147+
removeTexture(withPath: unresolvedPath)
148+
}
149+
123150
let importer = PNGImporter()
124151
try importer.synchronousPrepareToImportResourceFrom(path: unresolvedPath)
125152
let png = try importer.loadTexture(options: .none)
126153
let width = Int(png.size.width)
127154
let height = Int(png.size.height)
128-
let coord = searchGrid.firstUnoccupiedFor(
129-
width: textureBlocksWide(texturePixelWidth: png.size.width),
130-
height: textureBlocksTall(texturePixelHeight: png.size.height),
131-
markOccupied: true
132-
)
133-
let texture = Texture(resourcePath: unresolvedPath, width: width, height: height, imageData: png.data, coordinate: coord)
155+
156+
var textureData = TextureData(width: width, height: height, imageData: png.data, coordinate: (0,0))
157+
var dataIndex = self.textureDatas.endIndex
158+
if sacrificePerformanceForSize, let existingIndex = self.textureDatas.firstIndex(where: {$0 == textureData}) {
159+
dataIndex = existingIndex
160+
}else{
161+
let coord = searchGrid.firstUnoccupiedFor(
162+
width: textureBlocksWide(texturePixelWidth: png.size.width),
163+
height: textureBlocksTall(texturePixelHeight: png.size.height),
164+
markOccupied: true
165+
)
166+
textureData.coordinate = coord
167+
textureDatas.append(textureData)
168+
}
134169

135-
// Cleanup old values
136-
// If the texture has changed on disk we want to replace it
137-
removeTexture(withPath: unresolvedPath)
170+
let texture = Texture(resourcePath: unresolvedPath, dataIndex: dataIndex)
138171

139172
// Append new value
140173
self.textures.append(texture)
@@ -146,13 +179,33 @@ public final class TextureAtlasBuilder {
146179
@discardableResult
147180
public func removeTexture(withPath unresolvedPath: String) -> Bool {
148181
if let existing = textures.firstIndex(where: {$0.resourcePath == unresolvedPath}) {
182+
// Remove the texture
149183
let texture = self.textures.remove(at: existing)
150-
searchGrid.markAsOccupied(false,
151-
x: texture.coordinate.x,
152-
y: texture.coordinate.y,
153-
width: textureBlocksWide(texturePixelWidth: Float(texture.width)),
154-
height: textureBlocksTall(texturePixelHeight: Float(texture.height))
155-
)
184+
let textureData = self.textureDatas[texture.dataIndex]
185+
186+
// If the TextureData is no longer referenced, remove it
187+
if self.textures.contains(where: {$0.dataIndex == texture.dataIndex}) == false {
188+
// Free the grid area
189+
searchGrid.markAsOccupied(
190+
false,
191+
x: textureData.coordinate.x,
192+
y: textureData.coordinate.y,
193+
width: textureBlocksWide(texturePixelWidth: Float(textureData.width)),
194+
height: textureBlocksTall(texturePixelHeight: Float(textureData.height))
195+
)
196+
197+
// Reindex the Texture dataIndex values
198+
self.textures = self.textures.map({ texture in
199+
var texture = texture
200+
if texture.dataIndex > texture.dataIndex {
201+
texture.dataIndex -= 1
202+
}
203+
return texture
204+
})
205+
206+
// Remove the data
207+
self.textureDatas.remove(at: texture.dataIndex)
208+
}
156209

157210
self.needsGenerate = true
158211

@@ -169,12 +222,14 @@ public final class TextureAtlasBuilder {
169222
var imageData: Data = Data(repeating: 0, count: Int(textureSize.width * textureSize.height) * 4)
170223
imageData.withUnsafeMutableBytes { (bytes: UnsafeMutableRawBufferPointer) in
171224
for texture in textures {
172-
var coord = texture.coordinate
225+
let textureData = self.textureDatas[texture.dataIndex]
226+
227+
var coord = textureData.coordinate
173228
coord.x *= blockSize
174229
coord.y *= blockSize
175-
let srcWidth = texture.width * 4
230+
let srcWidth = textureData.width * 4
176231
let x = (coord.x * 4)
177-
for row in 0 ..< texture.height {
232+
for row in 0 ..< textureData.height {
178233
let srcStart = srcWidth * row
179234
let srcRange = srcStart ..< (srcStart + srcWidth)
180235

@@ -185,7 +240,7 @@ public final class TextureAtlasBuilder {
185240
let dstIndex = dstRange[i + dstRange.lowerBound]
186241
let srcIndex = srcRange[i + srcRange.lowerBound]
187242

188-
bytes[dstIndex] = texture.imageData[srcIndex]
243+
bytes[dstIndex] = textureData.imageData[srcIndex]
189244
}
190245
}
191246
}
@@ -198,10 +253,11 @@ public final class TextureAtlasBuilder {
198253
blockSize: blockSize,
199254
textures: textures.indices.map({
200255
let texture = textures[$0]
256+
let textureData = textureDatas[texture.dataIndex]
201257
return TextureAtlas.Texture(
202258
path: texture.resourcePath,
203-
size: Size2(Float(texture.width), Float(texture.height)),
204-
coordinate: texture.coordinate
259+
size: Size2(Float(textureData.width), Float(textureData.height)),
260+
coordinate: textureData.coordinate
205261
)
206262
})
207263
)

0 commit comments

Comments
 (0)