657 lines
18 KiB
Lua
657 lines
18 KiB
Lua
-- The demo kit: what the three demos share. A demo loads it with dofile(DIR .. "../kit.lua") and
|
|
-- gets back one table.
|
|
--
|
|
-- Time is the music's. K.time() is where the listener is in the track: musicGetPosition while the
|
|
-- demo plays, or a clock that moves exactly one frame a frame when DEMO_CAPTURE asks for a capture
|
|
-- (see util/demoCapture.sh), so a captured frame lands on the music added afterwards. Everything
|
|
-- else is read off that time: beats and bars from the beat list util/demoSync.py wrote, the drums,
|
|
-- the bass, the loudness and the brightness as envelopes from 0 to 1, and the drum hits as lists.
|
|
--
|
|
-- A demo is a list of scenes, each with the bars it spans: { from = 17, to = 33, enter = fn, draw =
|
|
-- fn, leave = fn }. K.run calls enter once as the scene starts, draw every frame it is on with the
|
|
-- time, the bar, and how far through it is from 0 to 1, and leave once as it ends. Scenes may
|
|
-- overlap, and are drawn in list order.
|
|
|
|
local K = {}
|
|
|
|
local ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
local ENVELOPES = { "kick", "snare", "hat", "bass", "level", "bright", "noteClear" }
|
|
local HIT_LISTS = { "kicks", "snares", "hats" }
|
|
local LEVELS = 63
|
|
local PIXEL_PNG = "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\x0b\x49\x44\x41\x54\x78\x9c\x63\xf8\x0f\x04\x00\x09\xfb\x03\xfd\xfb\x5e\x6b\x2b\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82"
|
|
local FONT_FILE = "Singe/FreeSansBold.ttf"
|
|
local TAIL = 2 -- Seconds the demo runs on after the music ends
|
|
|
|
local sync = nil
|
|
local music = nil
|
|
local fonts = {}
|
|
local widths = {}
|
|
local pixel = nil
|
|
local startTicks = nil
|
|
local lastTime = 0
|
|
local lastTicks = 0
|
|
local frames = 0
|
|
local stats = { second = 0, frames = 0, worst = 0, last = 0 }
|
|
|
|
K.W = 640
|
|
K.H = 480
|
|
|
|
|
|
-- The fractional bar at time t, counting from 1: bar 1 starts on the beat the sync data names.
|
|
function K.bar(t)
|
|
return (K.beat(t) - (sync.barStart - 1)) / 4 + 1
|
|
end
|
|
|
|
|
|
-- When bar b starts; fractional bars land between beats.
|
|
function K.barTime(b)
|
|
return K.beatTime((b - 1) * 4 + (sync.barStart - 1))
|
|
end
|
|
|
|
|
|
-- The fractional beat at time t, counting the first beat of the list as 0. Before the first beat
|
|
-- and after the last the spacing of the nearest two carries on.
|
|
function K.beat(t)
|
|
local beats = sync.beats
|
|
local n = #beats
|
|
local lo = 1
|
|
local hi = n
|
|
|
|
if t <= beats[1] then
|
|
return (t - beats[1]) / (beats[2] - beats[1])
|
|
end
|
|
if t >= beats[n] then
|
|
return n - 1 + (t - beats[n]) / (beats[n] - beats[n - 1])
|
|
end
|
|
while hi - lo > 1 do
|
|
local mid = (lo + hi) // 2
|
|
|
|
if beats[mid] <= t then
|
|
lo = mid
|
|
else
|
|
hi = mid
|
|
end
|
|
end
|
|
return lo - 1 + (t - beats[lo]) / (beats[hi] - beats[lo])
|
|
end
|
|
|
|
|
|
-- When fractional beat i falls.
|
|
function K.beatTime(i)
|
|
local beats = sync.beats
|
|
local n = #beats
|
|
local whole = math.floor(i)
|
|
local part = i - whole
|
|
|
|
if whole < 0 then
|
|
return beats[1] + i * (beats[2] - beats[1])
|
|
end
|
|
if whole >= n - 1 then
|
|
return beats[n] + (i - (n - 1)) * (beats[n] - beats[n - 1])
|
|
end
|
|
return beats[whole + 1] + part * (beats[whole + 2] - beats[whole + 1])
|
|
end
|
|
|
|
|
|
-- Vector bobs: count balls on a shape that changes every two bars (a ring, a helix, a sphere, a
|
|
-- flower), turned in 3D and drawn back to front at the size their distance gives them, each tinted
|
|
-- round the colour wheel. sprite is a white shaded ball; spread scales the shape; size is a ball's
|
|
-- radius in pixels at the shape's middle distance.
|
|
local BOB_DEPTH = 5
|
|
|
|
local function bobShape(kind, i, count)
|
|
local k = (i - 1) / count
|
|
local a = k * 2 * math.pi
|
|
|
|
if kind == 0 then
|
|
return math.cos(a) * 1.6, math.sin(a * 3) * 0.3, math.sin(a) * 1.6
|
|
elseif kind == 1 then
|
|
return math.cos(a * 3) * 1.0, (k - 0.5) * 3.2, math.sin(a * 3) * 1.0
|
|
elseif kind == 2 then
|
|
local phi = math.acos(1 - 2 * (i - 0.5) / count)
|
|
local theta = math.pi * (1 + math.sqrt(5)) * i
|
|
|
|
return math.cos(theta) * math.sin(phi) * 1.5, math.cos(phi) * 1.5, math.sin(theta) * math.sin(phi) * 1.5
|
|
end
|
|
local r = 1 + 0.5 * math.sin(a * 5)
|
|
|
|
return math.cos(a) * r * 1.4, math.sin(a) * r * 1.4, math.sin(a * 5) * 0.4
|
|
end
|
|
|
|
|
|
function K.bobs(t, bar, sprite, count, spread, size, cx, cy)
|
|
local shape = math.floor((bar - 1) / 2) % 4
|
|
local next = (shape + 1) % 4
|
|
local morph = K.smooth(0.75, 1.0, ((bar - 1) / 2) % 1)
|
|
local ay = t * 0.9
|
|
local ax = t * 0.5
|
|
local pts = {}
|
|
local pump = 1 + 0.25 * K.decay("kicks", t, 8)
|
|
|
|
for i = 1, count do
|
|
local x1, y1, z1 = bobShape(shape, i, count)
|
|
local x2, y2, z2 = bobShape(next, i, count)
|
|
local x = K.lerp(x1, x2, morph) * spread * pump
|
|
local y = K.lerp(y1, y2, morph) * spread * pump
|
|
local z = K.lerp(z1, z2, morph) * spread * pump
|
|
local x3 = x * math.cos(ay) + z * math.sin(ay)
|
|
local z3 = -x * math.sin(ay) + z * math.cos(ay)
|
|
local y3 = y * math.cos(ax) - z3 * math.sin(ax)
|
|
local z4 = y * math.sin(ax) + z3 * math.cos(ax)
|
|
|
|
pts[i] = { x = x3, y = y3, z = z4 + BOB_DEPTH, i = i }
|
|
end
|
|
table.sort(pts, function(a, b) return a.z > b.z end)
|
|
for _, p in ipairs(pts) do
|
|
local s = BOB_DEPTH / p.z
|
|
local r = size * s
|
|
local px = cx + p.x * s * 60
|
|
local py = cy + p.y * s * 60
|
|
local cr, cg, cb = K.hue(p.i / count + t * 0.1)
|
|
|
|
spriteSetTint(sprite, cr, cg, cb)
|
|
spriteDraw(sprite, math.floor(px - r), math.floor(py - r), math.floor(px + r), math.floor(py + r))
|
|
end
|
|
spriteSetTint(sprite, 255, 255, 255)
|
|
end
|
|
|
|
|
|
-- A filled rectangle, corners inclusive, in any colour and alpha: the kit's one-pixel sprite
|
|
-- stretched and tinted. blend is a SPRITE_ constant, alpha by default.
|
|
function K.box(x1, y1, x2, y2, r, g, b, a, blend)
|
|
if x2 < x1 or y2 < y1 then
|
|
return
|
|
end
|
|
spriteSetTint(pixel, r, g, b)
|
|
spriteSetAlpha(pixel, a or 255)
|
|
spriteSetBlend(pixel, blend or SPRITE_ALPHA)
|
|
spriteDraw(pixel, math.floor(x1), math.floor(y1), math.floor(x2), math.floor(y2))
|
|
end
|
|
|
|
|
|
-- A chrome material: metal, polished to roughness, tinted r, g, b. It needs a sky to reflect.
|
|
function K.chrome(r, g, b, roughness)
|
|
local m = materialNew()
|
|
|
|
materialSetColor(m, r, g, b)
|
|
materialSetMetallic(m, 1)
|
|
materialSetRoughness(m, roughness)
|
|
return m
|
|
end
|
|
|
|
|
|
function K.clamp(v, lo, hi)
|
|
if v < lo then
|
|
return lo
|
|
end
|
|
if v > hi then
|
|
return hi
|
|
end
|
|
return v
|
|
end
|
|
|
|
|
|
-- How long ago the most recent hit in a list ("kicks", "snares", "hats") was at time t; huge when
|
|
-- there has been none. An effect fed exp(-age * k) flashes on every hit and dies away.
|
|
function K.decay(list, t, k)
|
|
return math.exp(-K.hitAge(list, t) * k)
|
|
end
|
|
|
|
|
|
function K.ease(t)
|
|
t = K.clamp(t, 0, 1)
|
|
return t * t * (3 - 2 * t)
|
|
end
|
|
|
|
|
|
function K.easeIn(t)
|
|
t = K.clamp(t, 0, 1)
|
|
return t * t * t
|
|
end
|
|
|
|
|
|
function K.easeInOut(a, b, t)
|
|
return a + (b - a) * (0.5 - math.cos(K.clamp(t, 0, 1) * math.pi) / 2)
|
|
end
|
|
|
|
|
|
function K.easeOut(t)
|
|
t = 1 - K.clamp(t, 0, 1)
|
|
return 1 - t * t * t
|
|
end
|
|
|
|
|
|
-- An envelope's value at time t, 0 to 1: "kick", "snare", "hat", "bass", "level", "bright", or
|
|
-- "noteClear".
|
|
function K.env(name, t)
|
|
local curve = sync[name]
|
|
local f = t * sync.rate
|
|
local i = math.floor(f)
|
|
|
|
if i < 0 then
|
|
return curve[1]
|
|
end
|
|
if i >= #curve - 1 then
|
|
return curve[#curve]
|
|
end
|
|
return curve[i + 1] + (f - i) * (curve[i + 2] - curve[i + 1])
|
|
end
|
|
|
|
|
|
-- The whole screen in one colour: a flash, a fade, or a tint.
|
|
function K.flash(r, g, b, a, blend)
|
|
K.box(0, 0, K.W - 1, K.H - 1, r, g, b, a, blend)
|
|
end
|
|
|
|
|
|
-- Coloured glass: see-through by alpha, polished, drawn after everything solid.
|
|
function K.glass(r, g, b, alpha)
|
|
local m = materialNew()
|
|
|
|
materialSetColor(m, r, g, b, alpha or 90)
|
|
materialSetBlend(m, true)
|
|
materialSetRoughness(m, 0.05)
|
|
return m
|
|
end
|
|
|
|
|
|
-- Seconds since the most recent hit in a list at time t.
|
|
function K.hitAge(list, t)
|
|
local hits = sync[list]
|
|
local lo = 0
|
|
local hi = #hits + 1
|
|
|
|
while hi - lo > 1 do
|
|
local mid = (lo + hi) // 2
|
|
|
|
if hits[mid] <= t then
|
|
lo = mid
|
|
else
|
|
hi = mid
|
|
end
|
|
end
|
|
if lo == 0 then
|
|
return math.huge
|
|
end
|
|
return t - hits[lo]
|
|
end
|
|
|
|
|
|
-- How many hits in a list fall after t0 and up to t1: what a frame's worth of time held.
|
|
function K.hitCount(list, t0, t1)
|
|
local count = 0
|
|
|
|
for _, h in ipairs(sync[list]) do
|
|
if h > t1 then
|
|
break
|
|
end
|
|
if h > t0 then
|
|
count = count + 1
|
|
end
|
|
end
|
|
return count
|
|
end
|
|
|
|
|
|
-- A full-bright colour from a hue, 0 to 1 round the wheel.
|
|
function K.hue(h)
|
|
local f = (h % 1) * 6
|
|
local i = math.floor(f)
|
|
local p = f - i
|
|
local c = ({ { 1, p, 0 }, { 1 - p, 1, 0 }, { 0, 1, p }, { 0, 1 - p, 1 }, { p, 0, 1 }, { 1, 0, 1 - p } })[i + 1]
|
|
|
|
return math.floor(c[1] * 255), math.floor(c[2] * 255), math.floor(c[3] * 255)
|
|
end
|
|
|
|
|
|
function K.lerp(a, b, t)
|
|
return a + (b - a) * t
|
|
end
|
|
|
|
|
|
-- Reads the demo's sync data and its music, makes the overlay, and loads the kit's own pixel.
|
|
-- Envelopes become arrays of numbers from 0 to 1 here, once.
|
|
function K.load(dir, width, height)
|
|
local value = {}
|
|
|
|
K.W = width or K.W
|
|
K.H = height or K.H
|
|
overlaySetResolution(K.W, K.H)
|
|
for i = 1, #ALPHABET do
|
|
value[ALPHABET:byte(i)] = (i - 1) / LEVELS
|
|
end
|
|
sync = dofile(dir .. "sync.lua")
|
|
for _, name in ipairs(ENVELOPES) do
|
|
local text = sync[name]
|
|
local curve = {}
|
|
|
|
for i = 1, #text do
|
|
curve[i] = value[text:byte(i)]
|
|
end
|
|
sync[name] = curve
|
|
end
|
|
local notes = {}
|
|
|
|
for i = 1, #sync.note do
|
|
notes[i] = math.floor(value[sync.note:byte(i)] * LEVELS + 0.5)
|
|
end
|
|
sync.note = notes
|
|
for _, name in ipairs(HIT_LISTS) do
|
|
sync[name] = sync[name] or {}
|
|
end
|
|
K.duration = sync.duration
|
|
if not DEMO_CAPTURE then
|
|
music = musicLoad(dir .. "music.mp3")
|
|
end
|
|
pixel = spriteLoadData(PIXEL_PNG)
|
|
return sync
|
|
end
|
|
|
|
|
|
-- The strongest pitch class at time t (0 is C) and how clearly it stands out, 0 to 1.
|
|
function K.note(t)
|
|
local i = K.clamp(math.floor(t * sync.rate) + 1, 1, #sync.note)
|
|
|
|
return sync.note[i], sync.noteClear[i]
|
|
end
|
|
|
|
|
|
-- Every mesh node under a model instance gets the material.
|
|
function K.paint(node, material)
|
|
nodeSetMaterial(node, material)
|
|
for _, child in ipairs(nodeGetChildren(node)) do
|
|
K.paint(child, material)
|
|
end
|
|
end
|
|
|
|
|
|
-- Starts the music and the clock, and takes over onOverlayUpdate to run the scenes. Under
|
|
-- DEMO_CAPTURE = { from = seconds, to = seconds, every = frames } nothing plays: the clock starts
|
|
-- at from and moves by the frame (run Singe with --deterministic), every frame (or every "every"th)
|
|
-- is saved as frameNNNNNN.png, and the demo quits at to.
|
|
function K.run(scenes)
|
|
local active = {}
|
|
|
|
if DEMO_CAPTURE then
|
|
debugPrint(string.format("DEMO capture from %.3f to %.3f", DEMO_CAPTURE.from, DEMO_CAPTURE.to))
|
|
else
|
|
musicPlay(music)
|
|
end
|
|
function onOverlayUpdate()
|
|
local t = K.time()
|
|
local bar = K.bar(t)
|
|
|
|
frames = frames + 1
|
|
if DEMO_STATS then
|
|
K.stats(t, bar)
|
|
end
|
|
colorBackground(0, 0, 0, 0)
|
|
overlayClear()
|
|
for i, scene in ipairs(scenes) do
|
|
local on = bar >= scene.from and bar < scene.to
|
|
|
|
if on and not active[i] then
|
|
active[i] = true
|
|
if scene.enter then
|
|
scene.enter(t)
|
|
end
|
|
elseif not on and active[i] then
|
|
active[i] = false
|
|
if scene.leave then
|
|
scene.leave(t)
|
|
end
|
|
end
|
|
if on and scene.draw then
|
|
scene.draw(t, bar, (bar - scene.from) / (scene.to - scene.from))
|
|
end
|
|
end
|
|
if DEMO_CAPTURE then
|
|
if frames % (DEMO_CAPTURE.every or 1) == 0 then
|
|
singeScreenshot(string.format("frame%06d", frames))
|
|
end
|
|
if t >= DEMO_CAPTURE.to then
|
|
debugPrint(string.format("DEMO captured %d frames", frames))
|
|
singeQuit()
|
|
end
|
|
elseif t >= sync.duration + TAIL then
|
|
singeQuit()
|
|
end
|
|
return OVERLAY_UPDATED
|
|
end
|
|
end
|
|
|
|
|
|
-- Under DEMO_STATS, once a second: frames drawn in the last second, and the longest gap between two.
|
|
function K.stats(t, bar)
|
|
local now = singeGetTicks()
|
|
|
|
if stats.last > 0 then
|
|
stats.worst = math.max(stats.worst, now - stats.last)
|
|
end
|
|
stats.last = now
|
|
stats.frames = stats.frames + 1
|
|
if now - stats.second >= 1000 then
|
|
debugPrint(string.format("DEMO t %6.2f bar %6.2f %3d fps worst %3d ms", t, bar, stats.frames, stats.worst))
|
|
stats.second = now
|
|
stats.frames = 0
|
|
stats.worst = 0
|
|
end
|
|
end
|
|
|
|
|
|
-- A flat quad that exactly fills the camera's view at distance d (oversize makes it larger, to stay
|
|
-- covering while it turns): for a video over, or as, the whole picture in 3D, where it can turn and
|
|
-- glow. What comes back is a pivot on the camera's axis with the quad stood up under it, so the
|
|
-- pivot's roll, nodeSetRotation(q, 0, 0, angle), turns the quad in its own plane. It starts hidden.
|
|
function K.screenQuad(camera, fov, material, d, oversize)
|
|
local h = 2 * d * math.tan(math.rad(fov / 2)) * (oversize or 1)
|
|
local pivot = nodeNew(camera)
|
|
local quad = nodeNew(pivot)
|
|
|
|
nodeSetPosition(pivot, 0, 0, -d)
|
|
nodeSetMesh(quad, meshPlane(h * K.W / K.H, h), material)
|
|
nodeSetRotation(quad, 90, 0, 0)
|
|
nodeSetVisible(pivot, false)
|
|
return pivot
|
|
end
|
|
|
|
|
|
-- A sine scroller: each letter a sprite made once in the font, coloured by where it is on the
|
|
-- screen, riding a wave of amplitude amp. It enters from the right at t0 and runs at speed pixels
|
|
-- a second.
|
|
local glyphs = {}
|
|
|
|
function K.scroller(t, font, text, y, amp, speed, t0)
|
|
local x = K.W - (t - t0) * speed
|
|
|
|
for i = 1, #text do
|
|
local key = font .. text:sub(i, i)
|
|
local g = glyphs[key]
|
|
|
|
if not g then
|
|
fontSelect(font)
|
|
colorForeground(255, 255, 255, 255)
|
|
fontQuality(FONT_QUALITY_BLENDED)
|
|
local s = fontToSprite(text:sub(i, i))
|
|
|
|
g = { sprite = s, w = spriteGetWidth(s) }
|
|
glyphs[key] = g
|
|
end
|
|
if x > K.W then
|
|
break
|
|
end
|
|
if x + g.w > 0 then
|
|
local r, gg, b = K.hue(x / K.W * 0.5 + t * 0.2)
|
|
|
|
spriteSetTint(g.sprite, r, gg, b)
|
|
spriteDraw(g.sprite, math.floor(x), math.floor(y + math.sin(x * 0.018 + t * 4) * amp))
|
|
end
|
|
x = x + g.w
|
|
end
|
|
end
|
|
|
|
|
|
function K.smooth(edge0, edge1, x)
|
|
return K.ease((x - edge0) / (edge1 - edge0))
|
|
end
|
|
|
|
|
|
-- A full-screen flash that fires on every hit in a list and dies away at rate k: a = 0 is off.
|
|
function K.strobe(t, list, k, r, g, b, a)
|
|
local v = K.decay(list, t, k)
|
|
|
|
if v > 0.02 and (a or 255) > 0 then
|
|
K.flash(r, g, b, math.floor((a or 255) * v))
|
|
end
|
|
end
|
|
|
|
|
|
-- How wide text is in the bundled font at a point size, in pixels; measured once and remembered.
|
|
function K.textWidth(text, size)
|
|
local key = size .. ":" .. text
|
|
local w = widths[key]
|
|
|
|
if not w then
|
|
local font = fonts[size]
|
|
|
|
if not font then
|
|
font = fontLoad(FONT_FILE, size)
|
|
fonts[size] = font
|
|
end
|
|
fontSelect(font)
|
|
local sprite = fontToSprite(text)
|
|
|
|
w = spriteGetWidth(sprite)
|
|
widths[key] = w
|
|
spriteUnload(sprite)
|
|
end
|
|
return w
|
|
end
|
|
|
|
|
|
-- Text in the bundled font at a point size, in a colour, left, centred ("center"), or right
|
|
-- aligned on x.
|
|
function K.text(x, y, text, size, r, g, b, align)
|
|
local font = fonts[size]
|
|
|
|
if not font then
|
|
font = fontLoad(FONT_FILE, size)
|
|
fonts[size] = font
|
|
end
|
|
fontSelect(font)
|
|
fontQuality(FONT_QUALITY_BLENDED)
|
|
colorForeground(r, g, b, 255)
|
|
if align then
|
|
local w = K.textWidth(text, size)
|
|
|
|
fontSelect(font)
|
|
if align == "center" then
|
|
x = x - w // 2
|
|
else
|
|
x = x - w
|
|
end
|
|
end
|
|
fontPrint(math.floor(x), math.floor(y), text)
|
|
end
|
|
|
|
|
|
-- Where the listener is in the music, in seconds. It never runs backwards, which a mixer
|
|
-- position read just after a buffer moved can otherwise do by a millisecond, and once the music has
|
|
-- ended it runs on with the clock.
|
|
function K.time()
|
|
local now = singeGetTicks()
|
|
local t = 0
|
|
|
|
startTicks = startTicks or now
|
|
if DEMO_CAPTURE then
|
|
t = DEMO_CAPTURE.from + (now - startTicks) / 1000
|
|
else
|
|
t = musicGetPosition(music)
|
|
if t < 0 then
|
|
t = (lastTime > 0) and (lastTime + (now - lastTicks) / 1000) or 0
|
|
end
|
|
end
|
|
lastTime = math.max(lastTime, t)
|
|
lastTicks = now
|
|
return lastTime
|
|
end
|
|
|
|
|
|
-- A (2, 3) torus knot as a tube, built vertex by vertex for meshNew: the curve, a frame along it, and a ring
|
|
-- of vertices round each point.
|
|
function K.torusKnot(radius, tube, segments, sides)
|
|
local positions = {}
|
|
local normals = {}
|
|
local uvs = {}
|
|
local indices = {}
|
|
|
|
local function curve(phi)
|
|
local r = radius * (1 + 0.42 * math.cos(3 * phi))
|
|
|
|
return r * math.cos(2 * phi), r * math.sin(2 * phi), radius * 0.42 * math.sin(3 * phi)
|
|
end
|
|
|
|
for i = 0, segments - 1 do
|
|
local phi = i / segments * 2 * math.pi
|
|
local px, py, pz = curve(phi)
|
|
local qx, qy, qz = curve(phi + 0.01)
|
|
local tx = qx - px
|
|
local ty = qy - py
|
|
local tz = qz - pz
|
|
local tl = math.sqrt(tx * tx + ty * ty + tz * tz)
|
|
|
|
tx = tx / tl
|
|
ty = ty / tl
|
|
tz = tz / tl
|
|
-- A normal that never lines up with the tangent: the knot never runs straight up the z axis.
|
|
local nx = ty * 1 - tz * 0
|
|
local ny = tz * 0 - tx * 1
|
|
local nz = tx * 0 - ty * 0
|
|
local nl = math.sqrt(nx * nx + ny * ny + nz * nz) + 1e-9
|
|
|
|
nx = nx / nl
|
|
ny = ny / nl
|
|
nz = nz / nl
|
|
local bx = ty * nz - tz * ny
|
|
local by = tz * nx - tx * nz
|
|
local bz = tx * ny - ty * nx
|
|
|
|
for j = 0, sides - 1 do
|
|
local a = j / sides * 2 * math.pi
|
|
local cx = math.cos(a) * nx + math.sin(a) * bx
|
|
local cy = math.cos(a) * ny + math.sin(a) * by
|
|
local cz = math.cos(a) * nz + math.sin(a) * bz
|
|
|
|
positions[#positions + 1] = px + cx * tube
|
|
positions[#positions + 1] = py + cy * tube
|
|
positions[#positions + 1] = pz + cz * tube
|
|
normals[#normals + 1] = cx
|
|
normals[#normals + 1] = cy
|
|
normals[#normals + 1] = cz
|
|
uvs[#uvs + 1] = i / segments
|
|
uvs[#uvs + 1] = j / sides
|
|
end
|
|
end
|
|
for i = 0, segments - 1 do
|
|
local i2 = (i + 1) % segments
|
|
|
|
for j = 0, sides - 1 do
|
|
local j2 = (j + 1) % sides
|
|
local a = i * sides + j + 1
|
|
local b = i2 * sides + j + 1
|
|
local c = i2 * sides + j2 + 1
|
|
local d = i * sides + j2 + 1
|
|
|
|
indices[#indices + 1] = a
|
|
indices[#indices + 1] = b
|
|
indices[#indices + 1] = c
|
|
indices[#indices + 1] = a
|
|
indices[#indices + 1] = c
|
|
indices[#indices + 1] = d
|
|
end
|
|
end
|
|
return meshNew(positions, normals, uvs, indices)
|
|
end
|
|
|
|
|
|
return K
|