This repository was archived by the owner on Aug 14, 2026. It is now read-only.
forked from TeJota1337/DramaticShapeVoxelMod
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathStadiumFx.lua
More file actions
436 lines (403 loc) · 15.2 KB
/
Copy pathStadiumFx.lua
File metadata and controls
436 lines (403 loc) · 15.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
-- STADIUM battles: the generated fire and gas stand-ins.
--
-- A port of model_extract/pipeline/effects.py, plus the bind-pose measurement
-- build.py sizes them against.
--
-- IMPORTANT: nothing here is extracted game data. The real tail flame, mane
-- fire and gas are drawn by procedural callbacks that live in a different
-- fragment -- geo command 0x08 records an attachment point and
-- func_80014A60 calls node->unk_10, and the model file supplies only two
-- empty display lists plus zeroed scratch buffers for it to fill. Those
-- callbacks have not been ported, so the models genuinely contain no flame
-- mesh and no flame texture: Charmander's texture set is eyes, claws, teeth
-- and skin.
--
-- What follows is an ORIGINAL, procedurally generated replacement -- looping
-- flipbook noise on a pair of crossed quads, anchored to the exact bone the
-- callback hangs off so it sits where the real effect would and follows the
-- animation. Seeds derive from the species number, so a given Pokemon always
-- generates the same flame.
--
-- Which species get one is the game's own grouping: every species sharing a
-- callback shares an effect.
--
-- 0x810000D8 Charmander, Charmeleon, Charizard, Magmar, Moltres tail flame
-- 0x81000108 Ponyta, Rapidash, Moltres's wings small flame
-- 0x810000E0 Gastly (only) gas cloud
-- the mod namespace (see main.lua): V.require loads a sibling module
local V = ...
local StadiumFx = {}
local floor = math.floor
local sqrt = math.sqrt
local sin, cos = math.sin, math.cos
local char = string.char
local concat = table.concat
local pi = math.pi
local FIRE_TAIL = 0x810000D8
local FIRE_SMALL = 0x81000108
local AURA = 0x810000E0
-- Desired size as a fraction of the model's world-space height: length, width.
StadiumFx.SIZES = {
fire_tail = { 0.40, 0.22 },
fire_small = { 0.075, 0.042 },
gas = { 1.05, 1.05 },
}
-- ------- 32-bit exclusive-or, in arithmetic
--
-- The generator below is an xorshift, so it needs a real 32-bit xor and a
-- real 32-bit wrap. Written out rather than taken from LuaJIT's `bit`, which
-- works in SIGNED 32-bit and would need converting back on every step -- see
-- the same note in StadiumFragment.
local function bxor32(a, b)
local r, p = 0, 1
for _ = 1, 32 do
local x, y = a % 2, b % 2
if x ~= y then r = r + p end
a, b, p = floor(a / 2), floor(b / 2), p * 2
end
return r
end
-- ------- deterministic noise
local Rng = {}
Rng.__index = Rng
local function newRng(seed)
local s = seed % 0x100000000
if s == 0 then s = 0x9E3779B9 end
return setmetatable({ s = s }, Rng)
end
function Rng:next()
local x = self.s
x = bxor32(x, (x % 0x80000) * 0x2000) -- x ^= (x << 13)
x = bxor32(x, floor(x / 0x20000)) -- x ^= x >> 17
x = bxor32(x, (x % 0x8000000) * 0x20) -- x ^= (x << 5)
self.s = x % 0x100000000
return self.s
end
function Rng:unit()
return self:next() / 0x100000000
end
-- A w-by-h lattice of unit noise, consumed row by row so the sequence -- and
-- therefore the texture -- is reproducible.
local function lattice(rng, w, h)
local g = {}
for y = 1, h do
local row = {}
for x = 1, w do row[x] = rng:unit() end
g[y] = row
end
return g
end
local function smooth(t)
return t * t * (3 - 2 * t)
end
-- Bilinear value noise on a torus, so the field tiles in both axes.
local function sample(grid, x, y)
local h = #grid
local w = #grid[1]
local fx0, fy0 = floor(x), floor(y)
local x0, y0 = fx0 % w, fy0 % h
local x1, y1 = (x0 + 1) % w, (y0 + 1) % h
local fx, fy = smooth(x - fx0), smooth(y - fy0)
local r0, r1 = grid[y0 + 1], grid[y1 + 1]
local a = r0[x0 + 1] + (r0[x1 + 1] - r0[x0 + 1]) * fx
local b = r1[x0 + 1] + (r1[x1 + 1] - r1[x0 + 1]) * fx
return a + (b - a) * fy
end
-- Sum octaves of tileable noise.
local function fbm(grids, x, y, scale)
local total, amp, norm = 0.0, 1.0, 0.0
for i = 1, #grids do
local f = scale * 2 ^ (i - 1)
total = total + sample(grids[i], x * f, y * f) * amp
norm = norm + amp
amp = amp * 0.5
end
return total / norm
end
-- Intensity -> RGBA, through a piecewise ramp.
local function ramp(stops, t)
if t < 0.0 then t = 0.0 elseif t > 1.0 then t = 1.0 end
for i = 1, #stops - 1 do
local a, b = stops[i], stops[i + 1]
if t <= b[1] then
local k = 0.0
if b[1] ~= a[1] then k = (t - a[1]) / (b[1] - a[1]) end
return floor(a[2] + (b[2] - a[2]) * k), floor(a[3] + (b[3] - a[3]) * k),
floor(a[4] + (b[4] - a[4]) * k), floor(a[5] + (b[5] - a[5]) * k)
end
end
local last = stops[#stops]
return last[2], last[3], last[4], last[5]
end
local FIRE_RAMP = {
{ 0.00, 0, 0, 0, 0 },
{ 0.30, 120, 24, 8, 90 },
{ 0.52, 226, 78, 16, 205 },
{ 0.74, 252, 176, 44, 245 },
{ 1.00, 255, 246, 214, 255 },
}
local GAS_RAMP = {
{ 0.00, 0, 0, 0, 0 },
{ 0.34, 52, 26, 78, 70 },
{ 0.60, 96, 52, 140, 140 },
{ 0.82, 148, 96, 196, 190 },
{ 1.00, 208, 176, 236, 215 },
}
local TRANSPARENT = char(0, 0, 0, 0)
-- An upward-advected noise plume. Scrolling by an exact multiple of the
-- lattice over the frame count is what makes the loop seamless.
local function fireFrames(seed, w, h, frames, wisp)
wisp = wisp or 1.0
local rng = newRng(seed)
local grids = { lattice(rng, 8, 8), lattice(rng, 16, 16),
lattice(rng, 32, 32) }
local out = {}
for f = 0, frames - 1 do
local t = f / frames
local buf = {}
for i = 1, w * h do buf[i] = TRANSPARENT end
for y = 0, h - 1 do
local v = y / (h - 1) -- 0 at the base, 1 at the tip
-- plume envelope: wide and hot at the base, pinched at the tip
local taper = 1.0 - v
if taper < 0.0 then taper = 0.0 end
taper = taper ^ 0.42
for x = 0, w - 1 do
local u = (x / (w - 1)) * 2 - 1 -- -1 .. 1 across the flame
local denom = taper * 0.95
if denom < 0.10 then denom = 0.10 end
local radial = 1.0 - (u < 0 and -u or u) / denom
if radial > 0 then
radial = radial ^ 0.7
local n = fbm(grids, x / w, (y / h) - t, 3.0)
local lick = 0.55 + 0.75 * (n - 0.5) * wisp
local inten = radial * (0.55 + 0.8 * taper) * lick
inten = inten - 0.16 * v -- cool towards the tip
if inten > 0.02 then
local r, g, b, a = ramp(FIRE_RAMP, inten)
-- +Y in texture space is up
buf[(h - 1 - y) * w + x + 1] = char(r, g, b, a)
end
end
end
end
out[f + 1] = concat(buf)
end
return w, h, out
end
-- Slow swirling haze that fades out towards the rim.
local function gasFrames(seed, w, h, frames)
local rng = newRng(seed)
local grids = { lattice(rng, 8, 8), lattice(rng, 16, 16),
lattice(rng, 32, 32) }
local out = {}
for f = 0, frames - 1 do
local t = f / frames
local buf = {}
for i = 1, w * h do buf[i] = TRANSPARENT end
local ang = t * 2 * pi
local ca, sa = cos(ang), sin(ang)
for y = 0, h - 1 do
for x = 0, w - 1 do
local dx = (x / (w - 1)) * 2 - 1
local dy = (y / (h - 1)) * 2 - 1
local d = sqrt(dx * dx + dy * dy)
if d < 1.0 then
local falloff = (1.0 - d) ^ 0.85
-- rotate the sample point so the haze churns without popping
local sx = dx * ca - dy * sa
local sy = dx * sa + dy * ca
local n = fbm(grids, sx * 0.5 + 0.5, sy * 0.5 + 0.5 - t, 2.5)
local inten = falloff * (0.78 + 1.30 * (n - 0.44))
if inten > 0.03 then
local r, g, b, a = ramp(GAS_RAMP, inten)
buf[y * w + x + 1] = char(r, g, b, a)
end
end
end
end
out[f + 1] = concat(buf)
end
return w, h, out
end
-- Two quads at right angles, so the effect reads from any angle. `axis` picks
-- which bone-local direction the quad grows along: bone-local +X runs down the
-- limb, so a flame laid out along X comes out lying sideways, and 'y' is that
-- same quad turned a quarter left about Z, which stands it up. `centred`
-- straddles the origin instead of growing from it.
local function crossedQuads(bone, length, width, axis, centred)
local pos, uv, nrm, skin, idx = {}, {}, {}, {}, {}
local ST = { { 0, 0 }, { 1, 0 }, { 1, 1 }, { 0, 1 } }
local nv, ni = 0, 0
for q = 0, 1 do
local base = nv
for k = 1, 4 do
local s, t = ST[k][1], ST[k][2]
local a = (s - 0.5) * width
local b = centred and (t - 0.5) * length or t * length
local px, py, pz
if axis == "x" then
if q == 0 then px, py, pz = b, a, 0.0 else px, py, pz = b, 0.0, a end
else -- (x, y) -> (-y, x)
if q == 0 then px, py, pz = -a, b, 0.0 else px, py, pz = 0.0, b, a end
end
pos[nv * 3 + 1], pos[nv * 3 + 2], pos[nv * 3 + 3] = px, py, pz
uv[nv * 2 + 1], uv[nv * 2 + 2] = s, 1.0 - t
if q == 0 then
nrm[nv * 3 + 1], nrm[nv * 3 + 2], nrm[nv * 3 + 3] = 0.0, 0.0, 1.0
else
nrm[nv * 3 + 1], nrm[nv * 3 + 2], nrm[nv * 3 + 3] = 1.0, 0.0, 0.0
end
skin[nv + 1] = bone
nv = nv + 1
end
idx[ni + 1], idx[ni + 2], idx[ni + 3] = base, base + 1, base + 2
idx[ni + 4], idx[ni + 5], idx[ni + 6] = base, base + 2, base + 3
ni = ni + 6
end
return { pos = pos, uv = uv, nrm = nrm, skin = skin, nverts = nv,
idx = idx, nidx = ni }
end
-- ------- the bind pose these are sized against
--
-- build.py's bind_extent, kept in its own 4x4 column-major convention rather
-- than folded into StadiumBuild's 3x4 walk. The two agree -- they are the
-- same skeleton -- but the effect sizes come out of THIS one's per-bone scale
-- measurement, and rewriting it into the other convention is exactly the kind
-- of change that moves a byte without anyone noticing.
local function trs(t, r, s)
local function S(v) return sin(v / 32768 * pi) end
local function C(v) return cos(v / 32768 * pi) end
local sx, cx = S(r[1]), C(r[1])
local sy, cy = S(r[2]), C(r[2])
local sz, cz = S(r[3]), C(r[3])
return { cy * cz * s[1], cy * sz * s[1], -sy * s[1], 0,
(sx * sy * cz - cx * sz) * s[2], (sx * sy * sz + cx * cz) * s[2],
sx * cy * s[2], 0,
(cx * sy * cz + sx * sz) * s[3], (cx * sy * sz - sx * cz) * s[3],
cx * cy * s[3], 0,
t[1], t[2], t[3], 1 }
end
local function mul(a, b)
local r = {}
for c = 0, 3 do
for i = 1, 4 do
r[c * 4 + i] = a[i] * b[c * 4 + 1] + a[4 + i] * b[c * 4 + 2]
+ a[8 + i] * b[c * 4 + 3] + a[12 + i] * b[c * 4 + 4]
end
end
return r
end
-- (height of the bind pose, per-bone local scale). Height rather than the
-- largest dimension: sizing off the max would scale Moltres's flames to its
-- wingspan.
function StadiumFx.bindExtent(data)
local root = trs({ 0, 0, 0 }, { 0, 0, 0 }, data.rootScale)
local acc, uns, mats = {}, {}, {}
for i = 1, #data.bones do
local b = data.bones[i]
local p = b.parent
local pa = (p >= 0) and acc[p + 1] or { 1.0, 1.0, 1.0 }
local pu = (p >= 0) and uns[p + 1] or root
local u = mul(pu, trs({ b.t[1] * pa[1], b.t[2] * pa[2], b.t[3] * pa[3] },
b.r, { 1, 1, 1 }))
local a = { pa[1] * b.s[1], pa[2] * b.s[2], pa[3] * b.s[3] }
local m = {}
for k = 1, 16 do m[k] = u[k] end
for k = 1, 4 do
m[k] = m[k] * a[1]
m[4 + k] = m[4 + k] * a[2]
m[8 + k] = m[8 + k] * a[3]
end
acc[i], uns[i], mats[i] = a, u, m
end
local lo = { 1e9, 1e9, 1e9 }
local hi = { -1e9, -1e9, -1e9 }
for _, prim in ipairs(data.prims) do
local pos, skin = prim.pos, prim.skin
for i = 1, prim.nverts do
local m = mats[skin[i] + 1]
if m then
local x, y, z = pos[i * 3 - 2], pos[i * 3 - 1], pos[i * 3]
local wx = m[1] * x + m[5] * y + m[9] * z + m[13]
local wy = m[2] * x + m[6] * y + m[10] * z + m[14]
local wz = m[3] * x + m[7] * y + m[11] * z + m[15]
if wx < lo[1] then lo[1] = wx end
if wy < lo[2] then lo[2] = wy end
if wz < lo[3] then lo[3] = wz end
if wx > hi[1] then hi[1] = wx end
if wy > hi[2] then hi[2] = wy end
if wz > hi[3] then hi[3] = wz end
end
end
end
local extent = (lo[1] <= hi[1]) and (hi[2] - lo[2]) or 1.0
-- how much each bone scales its own local space, so an effect can divide it
-- back out and come out the size it asked for wherever it hangs
local scales = {}
for i = 1, #mats do
local m = mats[i]
scales[i] = sqrt(m[1] * m[1] + m[2] * m[2] + m[3] * m[3])
end
return extent, scales
end
-- ------- what a species gets
-- Returns a list of { kind, bone, geo, w, h, frames }, or an empty list.
function StadiumFx.buildFor(species, fx, extent, boneScale)
local out = {}
for _, node in ipairs(fx) do
local cb, bone = node.callback, node.bone
if bone >= 0 and bone < #boneScale then
local k = boneScale[bone + 1]
if k == 0 then k = 1.0 end
if cb == FIRE_TAIL then
local fl, fw = StadiumFx.SIZES.fire_tail[1], StadiumFx.SIZES.fire_tail[2]
local w, h, fr = fireFrames(species * 7919 + 1, 32, 64, 8)
out[#out + 1] = { kind = "fire", bone = bone, w = w, h = h, frames = fr,
geo = crossedQuads(bone, extent * fl / k,
extent * fw / k, "y", false) }
elseif cb == FIRE_SMALL then
local fl, fw = StadiumFx.SIZES.fire_small[1],
StadiumFx.SIZES.fire_small[2]
local w, h, fr = fireFrames(species * 6271 + bone, 24, 40, 8, 1.25)
out[#out + 1] = { kind = "fire", bone = bone, w = w, h = h, frames = fr,
geo = crossedQuads(bone, extent * fl / k,
extent * fw / k, "y", false) }
elseif cb == AURA and species == 92 then -- Gastly only
local fl, fw = StadiumFx.SIZES.gas[1], StadiumFx.SIZES.gas[2]
local w, h, fr = gasFrames(species * 5237 + 3, 48, 48, 10)
out[#out + 1] = { kind = "gas", bone = bone, w = w, h = h, frames = fr,
geo = crossedQuads(bone, extent * fl / k,
extent * fw / k, "y", true) }
end
end
end
return out
end
-- Append the generated prims and their flipbook textures to a model, exactly
-- as build.py's attach_effects does. Returns how many were made.
function StadiumFx.attach(data, species)
if not (data.fx and #data.fx > 0) then return 0 end
local extent, boneScale = StadiumFx.bindExtent(data)
local made = StadiumFx.buildFor(species, data.fx, extent, boneScale)
for _, e in ipairs(made) do
local first = #data.textures -- 0-based, as the file
for i = 1, #e.frames do
data.textures[first + i] = { index = -1, w = e.w, h = e.h,
generated = true, rgba = e.frames[i] }
end
local g = e.geo
local fxFrames = {}
for i = 1, #e.frames do fxFrames[i] = first + i - 1 end
data.prims[#data.prims + 1] = {
tex = first, cull = 0, texAnim = -1, texMap = nil,
generated = true, effect = e.kind,
blend = (e.kind == "fire") and "add" or "alpha",
fxFrames = fxFrames,
pos = g.pos, uv = g.uv, nrm = g.nrm, skin = g.skin, nverts = g.nverts,
idx = g.idx, nidx = g.nidx,
}
end
return #made
end
return StadiumFx