rbxts-vex 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "rbxts-vex",
3
+ "main": "src/init.luau",
4
+ "types": "src/index.d.ts",
5
+ "version": "1.0.0",
6
+ "description": "An efficient, performance-minded voxel destruction model. Created by EternalEthel/qrisquinn (same person).",
7
+ "license": "MIT",
8
+ "author": "kunosyn",
9
+ "keywords": [
10
+ "roblox-ts",
11
+ "rbxts",
12
+ "voxel",
13
+ "destruction"
14
+ ],
15
+ "files": [
16
+ "src"
17
+ ],
18
+ "scripts": {
19
+ "build": "rbxtsc --rojo dev.project.json"
20
+ },
21
+ "devDependencies": {
22
+ "@rbxts/compiler-types": "^3.0.0-types.0",
23
+ "@rbxts/types": "^1.0.907",
24
+ "roblox-ts": "^3.0.0",
25
+ "typescript": "^5.9.3"
26
+ }
27
+ }
@@ -0,0 +1,85 @@
1
+ --[[
2
+ Config
3
+
4
+ Manages default configuration and validates user-provided settings.
5
+ Centralizes all tunable parameters for the Vex system.
6
+ ]]
7
+
8
+ local Config = {}
9
+
10
+ Config.DEFAULTS = {
11
+ voxelSize = 1,
12
+ useGreedyMesh = true,
13
+ maxVoxels = 10000,
14
+ lifetime = nil,
15
+ material = nil,
16
+ collisionGroup = "Debris",
17
+ anchored = false,
18
+ weldAdjacent = true,
19
+ }
20
+
21
+ Config.LIMITS = {
22
+ MIN_VOXEL_SIZE = 0.1,
23
+ MAX_VOXEL_SIZE = 10,
24
+ MIN_LIFETIME = 1,
25
+ MAX_VOXELS = 50000,
26
+ }
27
+
28
+
29
+ function Config.validate(userConfig)
30
+ local validated = {}
31
+
32
+ for key, defaultValue in pairs(Config.DEFAULTS) do
33
+ local value = userConfig[key]
34
+
35
+ if value == nil then
36
+ validated[key] = defaultValue
37
+ else
38
+ validated[key] = value
39
+ end
40
+ end
41
+
42
+ if validated.voxelSize < Config.LIMITS.MIN_VOXEL_SIZE then
43
+ warn(string.format("voxelSize %.2f below minimum, clamping to %.2f",
44
+ validated.voxelSize, Config.LIMITS.MIN_VOXEL_SIZE))
45
+ validated.voxelSize = Config.LIMITS.MIN_VOXEL_SIZE
46
+ end
47
+
48
+ if validated.voxelSize > Config.LIMITS.MAX_VOXEL_SIZE then
49
+ warn(string.format("voxelSize %.2f above maximum, clamping to %.2f",
50
+ validated.voxelSize, Config.LIMITS.MAX_VOXEL_SIZE))
51
+ validated.voxelSize = Config.LIMITS.MAX_VOXEL_SIZE
52
+ end
53
+
54
+ if validated.maxVoxels > Config.LIMITS.MAX_VOXELS then
55
+ warn(string.format("maxVoxels %d above limit, clamping to %d",
56
+ validated.maxVoxels, Config.LIMITS.MAX_VOXELS))
57
+ validated.maxVoxels = Config.LIMITS.MAX_VOXELS
58
+ end
59
+
60
+ if validated.lifetime and validated.lifetime < Config.LIMITS.MIN_LIFETIME then
61
+ warn(string.format("lifetime %.1f below minimum, clamping to %.1f",
62
+ validated.lifetime, Config.LIMITS.MIN_LIFETIME))
63
+ validated.lifetime = Config.LIMITS.MIN_LIFETIME
64
+ end
65
+
66
+ return validated
67
+ end
68
+
69
+
70
+ function Config.merge(baseConfig, overrides)
71
+ local merged = {}
72
+
73
+ for key, value in pairs(baseConfig) do
74
+ merged[key] = value
75
+ end
76
+
77
+ for key, value in pairs(overrides) do
78
+ merged[key] = value
79
+ end
80
+
81
+ return merged
82
+ end
83
+
84
+
85
+ return Config
@@ -0,0 +1,189 @@
1
+ --[[
2
+ GreedyMesher
3
+
4
+ Implements greedy meshing algorithm to combine adjacent voxels
5
+ into larger parts, dramatically reducing part count.
6
+
7
+ Algorithm: Scans voxel grid layer by layer, merging rectangular
8
+ regions of same-material voxels into single parts.
9
+
10
+ API:
11
+ - GreedyMesher.mesh(grid) -> meshes[]
12
+ ]]
13
+
14
+ local GreedyMesher = {}
15
+
16
+
17
+ local function canMerge(voxel1, voxel2)
18
+ if not voxel1 or not voxel2 then
19
+ return false
20
+ end
21
+
22
+ if voxel1.material ~= voxel2.material then
23
+ return false
24
+ end
25
+
26
+ if math.abs(voxel1.color.R - voxel2.color.R) > 0.01 then
27
+ return false
28
+ end
29
+
30
+ if math.abs(voxel1.color.G - voxel2.color.G) > 0.01 then
31
+ return false
32
+ end
33
+
34
+ if math.abs(voxel1.color.B - voxel2.color.B) > 0.01 then
35
+ return false
36
+ end
37
+
38
+ return true
39
+ end
40
+
41
+
42
+ local function createMesh(minX, minY, minZ, maxX, maxY, maxZ, material, color, voxelSize)
43
+ return {
44
+ min = Vector3.new(minX, minY, minZ),
45
+ max = Vector3.new(maxX, maxY, maxZ),
46
+ size = Vector3.new(
47
+ (maxX - minX + 1) * voxelSize,
48
+ (maxY - minY + 1) * voxelSize,
49
+ (maxZ - minZ + 1) * voxelSize
50
+ ),
51
+ center = Vector3.new(
52
+ (minX + maxX) / 2 * voxelSize,
53
+ (minY + maxY) / 2 * voxelSize,
54
+ (minZ + maxZ) / 2 * voxelSize
55
+ ),
56
+ material = material,
57
+ color = color
58
+ }
59
+ end
60
+
61
+
62
+ local function getVoxelKey(x, y, z)
63
+ return string.format("%d,%d,%d", x, y, z)
64
+ end
65
+
66
+
67
+ function GreedyMesher.mesh(grid)
68
+ local meshes = {}
69
+ local processed = {}
70
+
71
+ local minX = grid.bounds.min.X
72
+ local minY = grid.bounds.min.Y
73
+ local minZ = grid.bounds.min.Z
74
+ local maxX = grid.bounds.max.X
75
+ local maxY = grid.bounds.max.Y
76
+ local maxZ = grid.bounds.max.Z
77
+
78
+ for y = minY, maxY do
79
+ for z = minZ, maxZ do
80
+ for x = minX, maxX do
81
+ local key = getVoxelKey(x, y, z)
82
+
83
+ if grid.voxels[key] and not processed[key] then
84
+ local voxel = grid.voxels[key]
85
+ local startX = x
86
+ local endX = x
87
+
88
+ while endX + 1 <= maxX do
89
+ local nextKey = getVoxelKey(endX + 1, y, z)
90
+ local nextVoxel = grid.voxels[nextKey]
91
+
92
+ if not nextVoxel or processed[nextKey] or not canMerge(voxel, nextVoxel) then
93
+ break
94
+ end
95
+
96
+ endX = endX + 1
97
+ end
98
+
99
+ local endZ = z
100
+ local canExpandZ = true
101
+
102
+ while canExpandZ and endZ + 1 <= maxZ do
103
+ for testX = startX, endX do
104
+ local testKey = getVoxelKey(testX, y, endZ + 1)
105
+ local testVoxel = grid.voxels[testKey]
106
+
107
+ if not testVoxel or processed[testKey] or not canMerge(voxel, testVoxel) then
108
+ canExpandZ = false
109
+ break
110
+ end
111
+ end
112
+
113
+ if canExpandZ then
114
+ endZ = endZ + 1
115
+ end
116
+ end
117
+
118
+ local endY = y
119
+ local canExpandY = true
120
+
121
+ while canExpandY and endY + 1 <= maxY do
122
+ for testZ = z, endZ do
123
+ for testX = startX, endX do
124
+ local testKey = getVoxelKey(testX, endY + 1, testZ)
125
+ local testVoxel = grid.voxels[testKey]
126
+
127
+ if not testVoxel or processed[testKey] or not canMerge(voxel, testVoxel) then
128
+ canExpandY = false
129
+ break
130
+ end
131
+ end
132
+
133
+ if not canExpandY then
134
+ break
135
+ end
136
+ end
137
+
138
+ if canExpandY then
139
+ endY = endY + 1
140
+ end
141
+ end
142
+
143
+ for markY = y, endY do
144
+ for markZ = z, endZ do
145
+ for markX = startX, endX do
146
+ local markKey = getVoxelKey(markX, markY, markZ)
147
+ processed[markKey] = true
148
+ end
149
+ end
150
+ end
151
+
152
+ local mesh = createMesh(
153
+ startX, y, z,
154
+ endX, endY, endZ,
155
+ voxel.material,
156
+ voxel.color,
157
+ grid.voxelSize
158
+ )
159
+
160
+ table.insert(meshes, mesh)
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ return meshes
167
+ end
168
+
169
+
170
+ function GreedyMesher.createNaiveMeshes(grid)
171
+ local meshes = {}
172
+
173
+ for key, voxel in pairs(grid.voxels) do
174
+ local mesh = createMesh(
175
+ voxel.x, voxel.y, voxel.z,
176
+ voxel.x, voxel.y, voxel.z,
177
+ voxel.material,
178
+ voxel.color,
179
+ grid.voxelSize
180
+ )
181
+
182
+ table.insert(meshes, mesh)
183
+ end
184
+
185
+ return meshes
186
+ end
187
+
188
+
189
+ return GreedyMesher
@@ -0,0 +1,246 @@
1
+ --[[
2
+ VoxelGrid
3
+
4
+ Handles conversion of parts into 3D voxel grids.
5
+ Manages coordinate transformations and grid data structures.
6
+
7
+ API:
8
+ - VoxelGrid.fromPart(part, voxelSize) -> grid
9
+ - VoxelGrid.fromModel(model, voxelSize) -> grid
10
+ ]]
11
+
12
+ local VoxelGrid = {}
13
+
14
+ local EPSILON = 0.01
15
+
16
+
17
+ local function createGrid()
18
+ return {
19
+ voxels = {},
20
+ bounds = {
21
+ min = Vector3.new(math.huge, math.huge, math.huge),
22
+ max = Vector3.new(-math.huge, -math.huge, -math.huge)
23
+ },
24
+ material = Enum.Material.Plastic,
25
+ color = Color3.new(0.5, 0.5, 0.5),
26
+ voxelSize = 1
27
+ }
28
+ end
29
+
30
+
31
+ local function worldToGrid(worldPos, origin, voxelSize)
32
+ local relative = worldPos - origin
33
+
34
+ return Vector3.new(
35
+ math.floor(relative.X / voxelSize + 0.5),
36
+ math.floor(relative.Y / voxelSize + 0.5),
37
+ math.floor(relative.Z / voxelSize + 0.5)
38
+ )
39
+ end
40
+
41
+
42
+ local function gridToWorld(gridPos, origin, voxelSize)
43
+ return origin + Vector3.new(
44
+ gridPos.X * voxelSize,
45
+ gridPos.Y * voxelSize,
46
+ gridPos.Z * voxelSize
47
+ )
48
+ end
49
+
50
+
51
+ local function getGridKey(x, y, z)
52
+ return string.format("%d,%d,%d", x, y, z)
53
+ end
54
+
55
+
56
+ local function addVoxel(grid, x, y, z, material, color)
57
+ local key = getGridKey(x, y, z)
58
+
59
+ grid.voxels[key] = {
60
+ x = x,
61
+ y = y,
62
+ z = z,
63
+ material = material or grid.material,
64
+ color = color or grid.color
65
+ }
66
+
67
+ grid.bounds.min = Vector3.new(
68
+ math.min(grid.bounds.min.X, x),
69
+ math.min(grid.bounds.min.Y, y),
70
+ math.min(grid.bounds.min.Z, z)
71
+ )
72
+
73
+ grid.bounds.max = Vector3.new(
74
+ math.max(grid.bounds.max.X, x),
75
+ math.max(grid.bounds.max.Y, y),
76
+ math.max(grid.bounds.max.Z, z)
77
+ )
78
+ end
79
+
80
+
81
+ function VoxelGrid.fromPart(part, voxelSize)
82
+ if not part:IsA("BasePart") then
83
+ warn("VoxelGrid.fromPart requires a BasePart")
84
+ return nil
85
+ end
86
+
87
+ local grid = createGrid()
88
+ grid.voxelSize = voxelSize
89
+ grid.material = part.Material
90
+ grid.color = part.Color
91
+
92
+ local size = part.Size
93
+ local cframe = part.CFrame
94
+
95
+ local origin = cframe.Position - Vector3.new(
96
+ size.X / 2,
97
+ size.Y / 2,
98
+ size.Z / 2
99
+ )
100
+
101
+ local numVoxelsX = math.max(1, math.ceil(size.X / voxelSize))
102
+ local numVoxelsY = math.max(1, math.ceil(size.Y / voxelSize))
103
+ local numVoxelsZ = math.max(1, math.ceil(size.Z / voxelSize))
104
+
105
+ for x = 0, numVoxelsX - 1 do
106
+ for y = 0, numVoxelsY - 1 do
107
+ for z = 0, numVoxelsZ - 1 do
108
+ local worldPos = origin + Vector3.new(
109
+ (x + 0.5) * voxelSize,
110
+ (y + 0.5) * voxelSize,
111
+ (z + 0.5) * voxelSize
112
+ )
113
+
114
+ local region = Region3.new(
115
+ worldPos - Vector3.new(voxelSize/2, voxelSize/2, voxelSize/2),
116
+ worldPos + Vector3.new(voxelSize/2, voxelSize/2, voxelSize/2)
117
+ ):ExpandToGrid(4)
118
+
119
+ local parts = workspace:FindPartsInRegion3(region, part, 1)
120
+
121
+ if #parts > 0 then
122
+ addVoxel(grid, x, y, z, part.Material, part.Color)
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ grid.origin = origin
129
+ grid.rotation = cframe - cframe.Position
130
+
131
+ return grid
132
+ end
133
+
134
+
135
+ function VoxelGrid.fromModel(model, voxelSize, maxVoxels)
136
+ if not model:IsA("Model") and not model:IsA("BasePart") then
137
+ warn("VoxelGrid.fromModel requires a Model or BasePart")
138
+ return nil
139
+ end
140
+
141
+ local parts = {}
142
+
143
+ if model:IsA("BasePart") then
144
+ table.insert(parts, model)
145
+ else
146
+ for _, descendant in ipairs(model:GetDescendants()) do
147
+ if descendant:IsA("BasePart") then
148
+ table.insert(parts, descendant)
149
+ end
150
+ end
151
+ end
152
+
153
+ if #parts == 0 then
154
+ warn("No parts found in model")
155
+ return nil
156
+ end
157
+
158
+ local grid = createGrid()
159
+ grid.voxelSize = voxelSize
160
+
161
+ local globalMin = Vector3.new(math.huge, math.huge, math.huge)
162
+ local globalMax = Vector3.new(-math.huge, -math.huge, -math.huge)
163
+
164
+ for _, part in ipairs(parts) do
165
+ local size = part.Size
166
+ local pos = part.Position
167
+
168
+ local partMin = pos - size / 2
169
+ local partMax = pos + size / 2
170
+
171
+ globalMin = Vector3.new(
172
+ math.min(globalMin.X, partMin.X),
173
+ math.min(globalMin.Y, partMin.Y),
174
+ math.min(globalMin.Z, partMin.Z)
175
+ )
176
+
177
+ globalMax = Vector3.new(
178
+ math.max(globalMax.X, partMax.X),
179
+ math.max(globalMax.Y, partMax.Y),
180
+ math.max(globalMax.Z, partMax.Z)
181
+ )
182
+ end
183
+
184
+ grid.origin = globalMin
185
+
186
+ local voxelCount = 0
187
+
188
+ for _, part in ipairs(parts) do
189
+ local size = part.Size
190
+ local cframe = part.CFrame
191
+
192
+ local numVoxelsX = math.max(1, math.ceil(size.X / voxelSize))
193
+ local numVoxelsY = math.max(1, math.ceil(size.Y / voxelSize))
194
+ local numVoxelsZ = math.max(1, math.ceil(size.Z / voxelSize))
195
+
196
+ for x = 0, numVoxelsX - 1 do
197
+ for y = 0, numVoxelsY - 1 do
198
+ for z = 0, numVoxelsZ - 1 do
199
+ if maxVoxels and voxelCount >= maxVoxels then
200
+ warn(string.format("Reached maxVoxels limit (%d), stopping voxelization", maxVoxels))
201
+ return grid
202
+ end
203
+
204
+ local localPos = Vector3.new(
205
+ (x - numVoxelsX/2 + 0.5) * voxelSize,
206
+ (y - numVoxelsY/2 + 0.5) * voxelSize,
207
+ (z - numVoxelsZ/2 + 0.5) * voxelSize
208
+ )
209
+
210
+ local worldPos = cframe:PointToWorldSpace(localPos)
211
+ local gridPos = worldToGrid(worldPos, grid.origin, voxelSize)
212
+
213
+ addVoxel(grid, gridPos.X, gridPos.Y, gridPos.Z, part.Material, part.Color)
214
+ voxelCount = voxelCount + 1
215
+ end
216
+ end
217
+ end
218
+ end
219
+
220
+ return grid
221
+ end
222
+
223
+
224
+ function VoxelGrid.getVoxelCount(grid)
225
+ local count = 0
226
+
227
+ for _ in pairs(grid.voxels) do
228
+ count = count + 1
229
+ end
230
+
231
+ return count
232
+ end
233
+
234
+
235
+ function VoxelGrid.getVoxel(grid, x, y, z)
236
+ local key = getGridKey(x, y, z)
237
+ return grid.voxels[key]
238
+ end
239
+
240
+
241
+ function VoxelGrid.iterator(grid)
242
+ return pairs(grid.voxels)
243
+ end
244
+
245
+
246
+ return VoxelGrid
@@ -0,0 +1,144 @@
1
+ --[[
2
+ VoxelPool
3
+
4
+ Object pool for voxel parts. Reuses parts instead of constantly
5
+ creating/destroying them for better performance.
6
+
7
+ API:
8
+ - VoxelPool.get(size, material, color, collisionGroup) -> Part
9
+ - VoxelPool.release(part)
10
+ - VoxelPool.clear()
11
+ ]]
12
+
13
+ local VoxelPool = {}
14
+
15
+ local pool = {}
16
+ local activeCount = 0
17
+
18
+ local MAX_POOL_SIZE = 1000
19
+ local CONTAINER_NAME = "VoxelDebris"
20
+
21
+
22
+ local function getContainer()
23
+ local container = workspace:FindFirstChild(CONTAINER_NAME)
24
+
25
+ if not container then
26
+ container = Instance.new("Folder")
27
+ container.Name = CONTAINER_NAME
28
+ container.Parent = workspace
29
+ end
30
+
31
+ return container
32
+ end
33
+
34
+
35
+ local function createKey(size, material, color)
36
+ return string.format("%.1f_%.1f_%.1f_%s_%d_%d_%d",
37
+ size.X, size.Y, size.Z,
38
+ tostring(material),
39
+ math.floor(color.R * 255),
40
+ math.floor(color.G * 255),
41
+ math.floor(color.B * 255)
42
+ )
43
+ end
44
+
45
+
46
+ function VoxelPool.get(size, material, color, collisionGroup)
47
+ local key = createKey(size, material, color)
48
+
49
+ if pool[key] and #pool[key] > 0 then
50
+ local part = table.remove(pool[key])
51
+ part.Anchored = false
52
+ part.CanCollide = true
53
+ part.Transparency = 0
54
+ part.Parent = getContainer()
55
+ activeCount = activeCount + 1
56
+
57
+ return part
58
+ end
59
+
60
+ local part = Instance.new("Part")
61
+ part.Size = size
62
+ part.Material = material
63
+ part.Color = color
64
+ part.TopSurface = Enum.SurfaceType.Smooth
65
+ part.BottomSurface = Enum.SurfaceType.Smooth
66
+ part.Anchored = false
67
+ part.CanCollide = true
68
+ part.Parent = getContainer()
69
+
70
+ if collisionGroup then
71
+ part.CollisionGroup = collisionGroup
72
+ end
73
+
74
+ activeCount = activeCount + 1
75
+
76
+ return part
77
+ end
78
+
79
+
80
+ function VoxelPool.release(part)
81
+ if not part or not part:IsA("BasePart") then
82
+ return
83
+ end
84
+
85
+ local key = createKey(part.Size, part.Material, part.Color)
86
+
87
+ if not pool[key] then
88
+ pool[key] = {}
89
+ end
90
+
91
+ if #pool[key] >= MAX_POOL_SIZE then
92
+ part:Destroy()
93
+ activeCount = math.max(0, activeCount - 1)
94
+ return
95
+ end
96
+
97
+ for _, child in ipairs(part:GetChildren()) do
98
+ if child:IsA("WeldConstraint") or child:IsA("Weld") then
99
+ child:Destroy()
100
+ end
101
+ end
102
+
103
+ part.Parent = nil
104
+ part.CFrame = CFrame.new(0, -10000, 0)
105
+ part.Anchored = true
106
+ part.CanCollide = false
107
+ part.Velocity = Vector3.new(0, 0, 0)
108
+ part.RotVelocity = Vector3.new(0, 0, 0)
109
+
110
+ table.insert(pool[key], part)
111
+ activeCount = math.max(0, activeCount - 1)
112
+ end
113
+
114
+
115
+ function VoxelPool.clear()
116
+ for key, parts in pairs(pool) do
117
+ for _, part in ipairs(parts) do
118
+ if part and part:IsA("Instance") then
119
+ part:Destroy()
120
+ end
121
+ end
122
+ end
123
+
124
+ pool = {}
125
+ activeCount = 0
126
+ end
127
+
128
+
129
+ function VoxelPool.getStats()
130
+ local pooledCount = 0
131
+
132
+ for _, parts in pairs(pool) do
133
+ pooledCount = pooledCount + #parts
134
+ end
135
+
136
+ return {
137
+ active = activeCount,
138
+ pooled = pooledCount,
139
+ total = activeCount + pooledCount
140
+ }
141
+ end
142
+
143
+
144
+ return VoxelPool
package/src/index.d.ts ADDED
@@ -0,0 +1,100 @@
1
+ export interface VexOptions {
2
+ /**
3
+ * Bigger voxels = better performance.
4
+ * @default 1
5
+ */
6
+ voxelSize?: number;
7
+
8
+ /**
9
+ * Default optimization option, keep it on.
10
+ * @default true
11
+ */
12
+ useGreedyMesh?: boolean;
13
+
14
+ /**
15
+ * Max number of voxels which can be created, mainly a safety measure to prevent lag.
16
+ * @default 10000
17
+ */
18
+ maxVoxels?: number;
19
+
20
+ /**
21
+ * How long before cleaning up voxels.
22
+ */
23
+ lifetime?: number;
24
+
25
+ /**
26
+ * The material of the voxels.
27
+ */
28
+ material?: Enum.Material;
29
+
30
+ /**
31
+ * The collision group of the voxels.
32
+ * @default "Debris"
33
+ */
34
+ collisionGroup?: string;
35
+
36
+ /**
37
+ * Whether the voxels should be anchored or not.
38
+ * @default false
39
+ */
40
+ anchored?: boolean;
41
+
42
+ /**
43
+ * Whether to weld adjacent voxels together.
44
+ * @default true
45
+ */
46
+ weldAdjacent?: boolean;
47
+ }
48
+
49
+ export interface VoxelStructure {
50
+ /**
51
+ * Destroys the VoxelStructure and its physical Model/BasePart.
52
+ */
53
+ Destroy(): void;
54
+
55
+ /**
56
+ * Cleans up the voxels in the pool.
57
+ */
58
+ Cleanup(): void;
59
+
60
+ /**
61
+ * Applies Force to voxels in the structure from a specified position.
62
+ * @param force The Force to apply to the voxels.
63
+ * @param position The position at which to apply Force from.
64
+ */
65
+ ApplyForce(force: Vector3, position: Vector3): void;
66
+
67
+ /**
68
+ * Gets the current number of voxels.
69
+ * @returns The current number of voxels.
70
+ */
71
+ GetVoxelCount(): number;
72
+ }
73
+
74
+ export interface VoxelPoolStats {
75
+ readonly active: number;
76
+ readonly pooled: number;
77
+ readonly total: number;
78
+ }
79
+
80
+ declare const Vex: {
81
+ /**
82
+ *
83
+ * @param source The BasePart or Model to voxelize.
84
+ * @param options Configuration settings.
85
+ * @returns A VoxelStructure instance.
86
+ */
87
+ new(source: BasePart | Model, options?: Readonly<VexOptions>): VoxelStructure;
88
+
89
+ /**
90
+ * @returns Stats about the current state of the voxel pool.
91
+ */
92
+ getPoolStats(): VoxelPoolStats;
93
+
94
+ /**
95
+ * Clears the voxel pool.
96
+ */
97
+ clearPool(): void;
98
+ }
99
+
100
+ export default Vex;
package/src/init.luau ADDED
@@ -0,0 +1,226 @@
1
+ --[[
2
+ Vex 2.0 - Main Module
3
+
4
+ High-performance voxel destruction system with greedy meshing
5
+ and object pooling.
6
+
7
+ API:
8
+ Vex.new(model, config) -> VoxelStructure
9
+
10
+ VoxelStructure:Destroy()
11
+ VoxelStructure:ApplyForce(force, position)
12
+ VoxelStructure:Cleanup()
13
+ VoxelStructure:GetVoxelCount()
14
+ ]]
15
+
16
+ local Vex = {}
17
+ Vex.__index = Vex
18
+
19
+ local Config = require(script.Config)
20
+ local VoxelGrid = require(script.VoxelGrid)
21
+ local GreedyMesher = require(script.GreedyMesher)
22
+ local VoxelPool = require(script.VoxelPool)
23
+
24
+ local VoxelStructure = {}
25
+ VoxelStructure.__index = VoxelStructure
26
+
27
+
28
+ function VoxelStructure.new(source, config)
29
+ local self = setmetatable({}, VoxelStructure)
30
+
31
+ self.source = source
32
+ self.config = Config.validate(config or {})
33
+ self.parts = {}
34
+ self.isDestroyed = false
35
+ self.grid = nil
36
+ self.cleanupConnection = nil
37
+
38
+ return self
39
+ end
40
+
41
+
42
+ function VoxelStructure:_generateGrid()
43
+ if self.grid then
44
+ return self.grid
45
+ end
46
+
47
+ local grid
48
+
49
+ if self.source:IsA("BasePart") then
50
+ grid = VoxelGrid.fromPart(self.source, self.config.voxelSize)
51
+ else
52
+ grid = VoxelGrid.fromModel(
53
+ self.source,
54
+ self.config.voxelSize,
55
+ self.config.maxVoxels
56
+ )
57
+ end
58
+
59
+ if not grid then
60
+ warn("Failed to generate voxel grid")
61
+ return nil
62
+ end
63
+
64
+ self.grid = grid
65
+
66
+ return grid
67
+ end
68
+
69
+
70
+ function VoxelStructure:_createParts(meshes)
71
+ local parts = {}
72
+
73
+ for _, mesh in ipairs(meshes) do
74
+ local material = self.config.material or mesh.material
75
+ local part = VoxelPool.get(
76
+ mesh.size,
77
+ material,
78
+ mesh.color,
79
+ self.config.collisionGroup
80
+ )
81
+
82
+ local worldCenter = self.grid.origin + mesh.center
83
+ part.CFrame = CFrame.new(worldCenter)
84
+ part.Anchored = self.config.anchored
85
+
86
+ table.insert(parts, part)
87
+ table.insert(self.parts, part)
88
+ end
89
+
90
+ return parts
91
+ end
92
+
93
+
94
+ function VoxelStructure:_weldAdjacent()
95
+ if not self.config.weldAdjacent then
96
+ return
97
+ end
98
+
99
+ local WELD_DISTANCE = self.config.voxelSize * 1.5
100
+
101
+ for i, part1 in ipairs(self.parts) do
102
+ for j = i + 1, #self.parts do
103
+ local part2 = self.parts[j]
104
+
105
+ local distance = (part1.Position - part2.Position).Magnitude
106
+
107
+ if distance <= WELD_DISTANCE then
108
+ local weld = Instance.new("WeldConstraint")
109
+ weld.Part0 = part1
110
+ weld.Part1 = part2
111
+ weld.Parent = part1
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+
118
+ function VoxelStructure:_setupLifetime()
119
+ if not self.config.lifetime then
120
+ return
121
+ end
122
+
123
+ task.delay(self.config.lifetime, function()
124
+ self:Cleanup()
125
+ end)
126
+ end
127
+
128
+
129
+ function VoxelStructure:Destroy()
130
+ if self.isDestroyed then
131
+ warn("Structure already destroyed")
132
+ return
133
+ end
134
+
135
+ local grid = self:_generateGrid()
136
+
137
+ if not grid then
138
+ return
139
+ end
140
+
141
+ local meshes
142
+
143
+ if self.config.useGreedyMesh then
144
+ meshes = GreedyMesher.mesh(grid)
145
+ else
146
+ meshes = GreedyMesher.createNaiveMeshes(grid)
147
+ end
148
+
149
+ self:_createParts(meshes)
150
+ self:_weldAdjacent()
151
+ self:_setupLifetime()
152
+
153
+ if self.source and self.source.Parent then
154
+ self.source:Destroy()
155
+ end
156
+
157
+ self.isDestroyed = true
158
+ end
159
+
160
+
161
+ function VoxelStructure:ApplyForce(force, position)
162
+ if not self.isDestroyed then
163
+ warn("Call :Destroy() before applying forces")
164
+ return
165
+ end
166
+
167
+ local FORCE_RADIUS = 20
168
+
169
+ for _, part in ipairs(self.parts) do
170
+ if part and part.Parent then
171
+ local distance = (part.Position - position).Magnitude
172
+
173
+ if distance <= FORCE_RADIUS then
174
+ local direction = (part.Position - position).Unit
175
+ local falloff = math.max(0, 1 - (distance / FORCE_RADIUS))
176
+ local scaledForce = force * falloff
177
+
178
+ part.AssemblyLinearVelocity = part.AssemblyLinearVelocity + direction * scaledForce.Magnitude
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+
185
+ function VoxelStructure:Cleanup()
186
+ for _, part in ipairs(self.parts) do
187
+ if part and part.Parent then
188
+ VoxelPool.release(part)
189
+ end
190
+ end
191
+
192
+ self.parts = {}
193
+
194
+ if self.cleanupConnection then
195
+ self.cleanupConnection:Disconnect()
196
+ self.cleanupConnection = nil
197
+ end
198
+ end
199
+
200
+
201
+ function VoxelStructure:GetVoxelCount()
202
+ return #self.parts
203
+ end
204
+
205
+
206
+ function Vex.new(source, config)
207
+ if not source then
208
+ warn("Vex.new requires a Model or BasePart")
209
+ return nil
210
+ end
211
+
212
+ return VoxelStructure.new(source, config)
213
+ end
214
+
215
+
216
+ function Vex.clearPool()
217
+ VoxelPool.clear()
218
+ end
219
+
220
+
221
+ function Vex.getPoolStats()
222
+ return VoxelPool.getStats()
223
+ end
224
+
225
+
226
+ return Vex