-- 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.png and quits. dofile("Singe/Framework.singe") local W = 1024 local H = 768 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