261 lines
8.5 KiB
Text
261 lines
8.5 KiB
Text
-- Renders the Neon Coast car with Singe's own 3D renderer, for util/neonCoastCar.sh to turn into a
|
|
-- sprite sheet. The car is built here from lofted cross sections, wheels, and trim: a low wedge
|
|
-- of a sports car, our own design. Each steering angle is drawn twice, over black and over white,
|
|
-- and the pair gives every pixel's true colour and alpha, glass and soft edges included.
|
|
--
|
|
-- Run headless with --deterministic; it saves car<angle><b|w>.png and quits.
|
|
dofile("Singe/Framework.singe")
|
|
|
|
local W = discGetWidth() -- The canvas the shell asks for (util/neonCoastCar.sh)
|
|
local H = discGetHeight()
|
|
local ANGLES = { -30, -20, -10, 0, 10, 20, 30 }
|
|
local RED = { 215, 14, 30 }
|
|
-- The traffic: the same car straight from behind in other paints (CAR_TRAFFIC set by the wrapper).
|
|
local PAINTS = { { 20, 110, 230 }, { 245, 200, 40 }, { 230, 230, 240 }, { 40, 190, 90 } }
|
|
|
|
-- Each picture to take: the car's angle and its paint.
|
|
local SHOTS = {}
|
|
|
|
if CAR_TRAFFIC then
|
|
for _, paint in ipairs(PAINTS) do
|
|
SHOTS[#SHOTS + 1] = { angle = 0, paint = paint }
|
|
end
|
|
else
|
|
for _, angle in ipairs(ANGLES) do
|
|
SHOTS[#SHOTS + 1] = { angle = angle, paint = RED }
|
|
end
|
|
end
|
|
local SETTLE = 3 -- Frames each picture is held before it is taken
|
|
|
|
local frames = 0
|
|
|
|
|
|
local function material(r, g, b, metallic, roughness)
|
|
local m = materialNew()
|
|
|
|
materialSetColor(m, r, g, b)
|
|
materialSetMetallic(m, metallic)
|
|
materialSetRoughness(m, roughness)
|
|
return m
|
|
end
|
|
|
|
|
|
-- A loft: rings of points (each a list of {x, y} at a z) joined into a closed skin, capped at both
|
|
-- ends. Normals are left to meshNew, so the skin shades smoothly.
|
|
local function loft(stations)
|
|
local positions = {}
|
|
local indices = {}
|
|
local ring = #stations[1].points
|
|
|
|
for _, s in ipairs(stations) do
|
|
for _, p in ipairs(s.points) do
|
|
positions[#positions + 1] = p[1]
|
|
positions[#positions + 1] = p[2]
|
|
positions[#positions + 1] = s.z
|
|
end
|
|
end
|
|
for i = 0, #stations - 2 do
|
|
for j = 0, ring - 1 do
|
|
local a = i * ring + j + 1
|
|
local b = i * ring + (j + 1) % ring + 1
|
|
local c = (i + 1) * ring + (j + 1) % ring + 1
|
|
local d = (i + 1) * ring + j + 1
|
|
|
|
indices[#indices + 1] = a
|
|
indices[#indices + 1] = c
|
|
indices[#indices + 1] = d
|
|
indices[#indices + 1] = a
|
|
indices[#indices + 1] = b
|
|
indices[#indices + 1] = c
|
|
end
|
|
end
|
|
-- Caps: a fan from each end ring's middle.
|
|
for _, e in ipairs({ { 0, false }, { #stations - 1, true } }) do
|
|
local s = stations[e[1] + 1]
|
|
local cx = 0
|
|
local cy = 0
|
|
|
|
for _, p in ipairs(s.points) do
|
|
cx = cx + p[1] / ring
|
|
cy = cy + p[2] / ring
|
|
end
|
|
positions[#positions + 1] = cx
|
|
positions[#positions + 1] = cy
|
|
positions[#positions + 1] = s.z
|
|
local centre = #positions / 3
|
|
|
|
for j = 0, ring - 1 do
|
|
local a = e[1] * ring + j + 1
|
|
local b = e[1] * ring + (j + 1) % ring + 1
|
|
|
|
indices[#indices + 1] = centre
|
|
if e[2] then
|
|
indices[#indices + 1] = a
|
|
indices[#indices + 1] = b
|
|
else
|
|
indices[#indices + 1] = b
|
|
indices[#indices + 1] = a
|
|
end
|
|
end
|
|
end
|
|
return meshNew(positions, nil, nil, indices)
|
|
end
|
|
|
|
|
|
-- The body's cross section at half width w, beltline belt, sill sill: a rounded shoulder, slab
|
|
-- sides tucking in at the bottom, a flat floor.
|
|
local function bodySection(z, w, belt, sill)
|
|
return {
|
|
z = z,
|
|
points = {
|
|
{ 0, belt + 0.05 }, { -w * 0.55, belt + 0.03 }, { -w * 0.88, belt - 0.01 }, { -w, belt - 0.12 },
|
|
{ -w, sill + 0.14 }, { -w * 0.93, sill }, { w * 0.93, sill }, { w, sill + 0.14 },
|
|
{ w, belt - 0.12 }, { w * 0.88, belt - 0.01 }, { w * 0.55, belt + 0.03 },
|
|
},
|
|
}
|
|
end
|
|
|
|
|
|
local function cabinSection(z, w, base, top)
|
|
return {
|
|
z = z,
|
|
points = { { 0, top }, { -w * 0.7, top - 0.03 }, { -w, base + (top - base) * 0.25 }, { -w * 1.02, base }, { w * 1.02, base }, { w, base + (top - base) * 0.25 }, { w * 0.7, top - 0.03 } },
|
|
}
|
|
end
|
|
|
|
|
|
local function part(mesh, mat, x, y, z, rx, ry, rz)
|
|
local n = nodeNew(CAR)
|
|
|
|
nodeSetMesh(n, mesh, mat)
|
|
nodeSetPosition(n, x, y, z)
|
|
nodeSetRotation(n, rx or 0, ry or 0, rz or 0)
|
|
return n
|
|
end
|
|
|
|
|
|
local function buildCar()
|
|
local paint = material(RED[1], RED[2], RED[3], 0.55, 0.14)
|
|
|
|
PAINT = paint
|
|
local glass = material(14, 18, 30, 0.9, 0.06)
|
|
local black = material(12, 12, 14, 0, 0.6)
|
|
local trim = material(30, 30, 34, 0.5, 0.35)
|
|
local chrome = material(220, 220, 230, 1, 0.12)
|
|
local tyre = material(18, 18, 20, 0, 0.85)
|
|
local lamp = material(60, 0, 0, 0, 0.3)
|
|
local bright = material(90, 10, 10, 0, 0.3)
|
|
local plate = material(230, 230, 220, 0, 0.5)
|
|
|
|
materialSetEmissive(lamp, 255, 20, 40)
|
|
materialSetEmissive(bright, 255, 170, 170)
|
|
CAR = nodeNew()
|
|
|
|
-- The body: a wedge, low and narrow at the nose, widest over the rear wheels, a flat tail.
|
|
local sill = 0.2
|
|
local body = {
|
|
bodySection(-2.25, 0.70, 0.50, sill + 0.08), bodySection(-2.05, 0.86, 0.55, sill), bodySection(-1.6, 0.94, 0.60, sill),
|
|
bodySection(-0.9, 0.97, 0.66, sill), bodySection(-0.1, 0.98, 0.70, sill), bodySection(0.7, 1.0, 0.73, sill),
|
|
bodySection(1.4, 1.02, 0.76, sill), bodySection(1.95, 1.0, 0.78, sill), bodySection(2.2, 0.97, 0.77, sill + 0.03),
|
|
}
|
|
|
|
part(loft(body), paint, 0, 0, 0)
|
|
-- The cabin: all glass, a raked screen and a sloping rear window, with a painted roof on top.
|
|
local cabin = {
|
|
cabinSection(-0.95, 0.80, 0.64, 0.66), cabinSection(-0.35, 0.74, 0.68, 1.10), cabinSection(0.45, 0.70, 0.71, 1.14),
|
|
cabinSection(1.05, 0.74, 0.74, 1.02), cabinSection(1.55, 0.84, 0.76, 0.79),
|
|
}
|
|
|
|
part(loft(cabin), glass, 0, 0, 0)
|
|
part(meshBox(1.24, 0.035, 0.85), paint, 0, 1.135, 0.08)
|
|
-- The tail: a black panel, a full-width lamp bar with brighter cores, a plate, and twin pipes.
|
|
part(meshBox(1.86, 0.30, 0.04), black, 0, 0.53, 2.205)
|
|
part(meshBox(1.78, 0.11, 0.05), lamp, 0, 0.62, 2.22)
|
|
part(meshBox(0.36, 0.06, 0.05), bright, -0.62, 0.62, 2.235)
|
|
part(meshBox(0.36, 0.06, 0.05), bright, 0.62, 0.62, 2.235)
|
|
for i = -3, 3 do
|
|
part(meshBox(0.02, 0.09, 0.03), black, i * 0.12, 0.62, 2.25)
|
|
end
|
|
part(meshBox(0.52, 0.12, 0.02), plate, 0, 0.43, 2.23)
|
|
part(meshCylinder(0.065, 0.3, 16), chrome, -0.48, 0.27, 2.2, 90, 0, 0)
|
|
part(meshCylinder(0.065, 0.3, 16), chrome, 0.48, 0.27, 2.2, 90, 0, 0)
|
|
-- A low wing across the tail on two posts.
|
|
part(meshBox(1.8, 0.05, 0.3), paint, 0, 0.97, 2.0)
|
|
part(meshBox(0.05, 0.18, 0.1), trim, -0.6, 0.87, 2.0)
|
|
part(meshBox(0.05, 0.18, 0.1), trim, 0.6, 0.87, 2.0)
|
|
-- Intake strakes along each rear flank, and black skirts under the doors.
|
|
for side = -1, 1, 2 do
|
|
for i = 0, 4 do
|
|
part(meshBox(0.04, 0.035, 1.25), black, side * 1.005, 0.40 + i * 0.065, 0.45)
|
|
end
|
|
part(meshBox(0.05, 0.12, 2.3), black, side * 0.95, 0.27, 0.05)
|
|
end
|
|
-- Mirrors.
|
|
part(meshBox(0.16, 0.08, 0.12), paint, -0.98, 0.82, -0.55)
|
|
part(meshBox(0.16, 0.08, 0.12), paint, 0.98, 0.82, -0.55)
|
|
-- Wheels: wide tyres with chrome rims, the rears wider.
|
|
for _, w in ipairs({ { -0.9, -1.35, 0.25 }, { 0.9, -1.35, 0.25 }, { -0.92, 1.38, 0.32 }, { 0.92, 1.38, 0.32 } }) do
|
|
part(meshCylinder(0.34, w[3], 28), tyre, w[1], 0.34, w[2], 0, 0, 90)
|
|
part(meshCylinder(0.22, w[3] + 0.01, 20), chrome, w[1] + (w[1] > 0 and 0.002 or -0.002), 0.34, w[2], 0, 0, 90)
|
|
end
|
|
-- Under the car, a dark floor so the light does not show through.
|
|
part(meshBox(1.8, 0.04, 4.2), black, 0, 0.2, 0)
|
|
end
|
|
|
|
|
|
sceneEnable(true)
|
|
sceneSetAmbient(70, 60, 80)
|
|
sceneSetAntialias(true)
|
|
-- No tone curve, so the white background comes out white and the matte is exact.
|
|
sceneSetTonemap(TONEMAP_NONE)
|
|
buildCar()
|
|
|
|
local key = lightNew(LIGHT_DIRECTIONAL)
|
|
|
|
nodeSetPosition(key, 3, 8, -4)
|
|
nodeLookAt(key, 0, 0, 0)
|
|
lightSetIntensity(key, 2.6)
|
|
lightSetColor(key, 255, 230, 240)
|
|
local fill = lightNew(LIGHT_POINT)
|
|
|
|
nodeSetPosition(fill, -3, 3, 8)
|
|
lightSetIntensity(fill, 40)
|
|
lightSetColor(fill, 120, 160, 255)
|
|
local rim = lightNew(LIGHT_POINT)
|
|
|
|
nodeSetPosition(rim, 4, 2.5, 6)
|
|
lightSetIntensity(rim, 30)
|
|
lightSetColor(rim, 255, 80, 200)
|
|
local top = lightNew(LIGHT_POINT)
|
|
|
|
nodeSetPosition(top, 0, 6, 3)
|
|
lightSetIntensity(top, 25)
|
|
|
|
local camera = nodeNew()
|
|
|
|
cameraSet(camera)
|
|
cameraSetPerspective(26, 0.1, 100)
|
|
nodeSetPosition(camera, 0, 3.1, 11)
|
|
nodeLookAt(camera, 0, 0.55, 0)
|
|
overlaySetResolution(W, H)
|
|
|
|
|
|
function onOverlayUpdate()
|
|
local shot = frames // SETTLE
|
|
local take = SHOTS[shot // 2 + 1]
|
|
local white = shot % 2 == 1
|
|
|
|
frames = frames + 1
|
|
overlayClear()
|
|
if not take then
|
|
singeQuit()
|
|
return OVERLAY_UPDATED
|
|
end
|
|
nodeSetRotation(CAR, 0, take.angle, 0)
|
|
materialSetColor(PAINT, take.paint[1], take.paint[2], take.paint[3])
|
|
sceneSetBackground(white and 255 or 0, white and 255 or 0, white and 255 or 0, 255)
|
|
if frames % SETTLE == 0 then
|
|
singeScreenshot(string.format("shot%02d%s", shot // 2 + 1, white and "w" or "b"))
|
|
end
|
|
return OVERLAY_UPDATED
|
|
end
|