`,]']±>Sï}WÿÍu§ðp8·d%qy)6o;]ÉA¦ösï÷ôôô”°ÂVl†3dG-- title: alpha compositing -- author: Starchaser -- desc: Transparent tech demo -- script: lua -- license: ask -- version: 0.1 -- script: lua -- Axis FYI: -- +z -- -- +y -- +x -x -- -y -- -- -z -- Copy point function copyp(p) return {x=p.x,y=p.y,z=p.z} end -- Copy color function copyc(c,alpha) a=c[4] if alpha~=nil then a=alpha end return {c[1],c[2],c[3],a} end -- Copy triangle; alpha=alpha override function copyt(t,alpha) local t1={ id=t.id, parent=t.parent, face=t.face, color=copyc(t.color,alpha), texture=t.texture } for j,p in ipairs(t) do t1[j]=copyp(p) end return t1 end -- Used to differentiate shapes after -- combine()d; helps with double-draw ID=0 -- Useful if you want to change *part* -- of a shape directly (no copy). -- Otherwise, just call copy() function subset(s,cs,ce,copy,alpha) s2={ id=s.id } if copy then ID=ID+1 s2.id=ID end for i=cs,ce do local x=i-cs+1 local t=s[i] if copy then if not s2[x] then table.insert(s2,{}) end s2[x]=copyt(t,alpha) if t.parent==nil then s2[x].parent=s2.id end else table.insert(s2,t) end end return s2 end function copy(s,alpha) return subset(s,1,#s,true,alpha) end -- New triangle -- Useful when you want to generate -- multiple triangles from the same -- set of points but want them safe -- to individually transform. function newt(p1,p2,p3,color) return {copyp(p1),copyp(p2),copyp(p3),color=color} end -- Combine two shapes together -- Useful for ensuring proper z-order -- when drawing nested shapes. -- Set copy to true if you also intend -- to do more transforms while also -- displaying the originals. function combine(s1,s2,copy) local s3=s1 if copy then s3=copy(s1) end for i,t in ipairs(s2) do if copy then table.insert(s3,copyt(t)) else table.insert(s3,t) end end return s3 end function translate(s,x,y,z) for i,t in ipairs(s) do for j,p in ipairs(t) do p.x=p.x+x p.y=p.y+y p.z=p.z+z end end end -- Matrix multiplication function mmult(m1,m2) local m3={} tc=#m2[1] sc=#m1[1] for r=1,#m1 do m3[r]={} for c=1,tc do local v=0 for n=1,sc do v=v+m1[r][n]*m2[n][c] end m3[r][c]=v end end return m3 end -- Apply f() on all points in a given -- shape. Useful for specifying a -- custom transform. See scale() etc. function apply(s,f) for i,t in ipairs(s) do for j,p in ipairs(t) do local r=f(p) p.x=r[1][1] p.y=r[1][2] p.z=r[1][3] end end end function scale(s,x,y,z) apply( s, function (p) return mmult({{p.x,p.y,p.z}},{ {x,0,0}, {0,y,0}, {0,0,z} }) end ) end -- May not work as expected...? function shear(s,x,y,z) local ms={ {1,x*y,x*z}, {y*x,1,y*z}, {z*x,z*y,1} } apply( s, function (p) return mmult({{p.x,p.y,p.z}},ms) end ) end -- 0 safe. Specify zero for axes you -- don't want rotated. function rotate(s,x,y,z) local dx=math.rad(x) local dy=math.rad(y) local dz=math.rad(z) local mx={ {1,0,0}, {0,math.cos(dx),-math.sin(dx)}, {0,math.sin(dx),math.cos(dx)} } local my={ {math.cos(dy),0,math.sin(dy)}, {0,1,0}, {-math.sin(dy),0,math.cos(dy)} } local mz={ {math.cos(dz),-math.sin(dz),0}, {math.sin(dz),math.cos(dz),0}, {0,0,1}, } apply( s, function (p) return mmult(mmult(mmult( {{p.x,p.y,p.z}},mx),my),mz) end ) end -- Useful for implementing 3D versions -- of line() etc. If you do, remember -- to write the drawn results to the -- pixels map to get full compositing. function project(p) local f=p.z-c.z local x1=((p.x-c.x)*(f/p.z))+c.x local y1=((p.y-c.y)*(f/p.z))+c.y return {x=x1,y=y1} end function vsub(p1,p2) return {x=p1.x-p2.x,y=p1.y-p2.y,z=(p1.z or 0)-(p2.z or 0)} end -- Triangles aren't culled because -- transparency could revel them. But -- this is used for an optimization -- to avoid double-draw on same-shape -- triangles meeting at corners. function cull(d1,d2,d3) local ab=vsub(d2,d1) local ac=vsub(d3,d1) return ab.x*ac.y-ac.x*ab.y<0; end -- Clever! Thanks, Pedro Gimeno -- https://stackoverflow.com/a/58411671/251262 function round(num) return num+(2^52+2^51)-(2^52+2^51) end -- Check for points outside the screen function isOut(p) return p.x<0 or p.y<0 or p.x>239 or p.y>135 end -- Could be upgraded to use a field of -- view "cone" to calculate what's out -- of view. -- -- Filter out things behind the camera -- and accidentally untranslated items -- that can hang drawTriangle. function kill(t,d1,d2,d3) return ( t[1].z<0 or t[2].z<0 or t[3].z<0 or d1.x==math.huge or d1.x==-math.huge or d1.y==math.huge or d1.y==-math.huge or d2.x==math.huge or d2.x==-math.huge or d2.y==math.huge or d2.y==-math.huge or d3.x==math.huge or d3.x==-math.huge or d3.y==math.huge or d3.y==-math.huge or isOut(d1) and isOut(d2) and isOut(d3) ) end -- Magic function drawTriangle(t) local d1=project(t[1]) local d2=project(t[2]) local d3=project(t[3]) local xyxy={ round(math.min(d1.x,d2.x,d3.x)), round(math.min(d1.y,d2.y,d3.y)), round(math.max(d1.x,d2.x,d3.x)), round(math.max(d1.y,d2.y,d3.y)) } local texture=t.texture if not kill(t,d1,d2,d3) then local culled=cull(d1,d2,d3) local color=1 if color==CLEAR then color=0 end if texture then local t1=t.texture and t.texture[1] local t2=t.texture and t.texture[2] local t3=t.texture and t.texture[3] ttri( d1.x,d1.y,d2.x,d2.y,d3.x,d3.y, t1.x,t1.y,t2.x,t2.y,t3.x,t3.y, 0, t.texture.bg or -1 ) else tri(d1.x,d1.y,d2.x,d2.y,d3.x,d3.y,color) end pixels.miny=math.min(xyxy[2],pixels.miny) pixels.maxy=math.max(xyxy[4],pixels.maxy) for y=xyxy[2],xyxy[4] do for x=xyxy[1],xyxy[3] do local idx=y*240+x local p=pix(x,y) if p~=CLEAR then if AVOID_DOUBLE_DRAW then -- BEGIN OPTIMIZATION HELL if pixels[idx]==nil then pixels[idx]={ cullchk={}, } end if pixels[idx].cullchk[t.parent]==nil then pixels[idx].cullchk[t.parent]={} end -- In theory all triangles making -- up a shape shouldn't color the -- same pixel twice, so let's try -- to adjust for our reality. local face=pixels[idx].face local cullchk=pixels[idx].cullchk[t.parent][culled] -- A single face, if composed of -- multiple triangles, shouldn't -- color the same pixel twice. if face==nil or face~=t.face then -- Triangles in the same 'half' -- of an object shouldn't color -- the same pixel twice, so take -- the latest color. if cullchk==nil or cullchk~=culled then table.insert(pixels[idx],t.color or palette[p+1]) else pixels[idx][#pixels[idx]]=t.color or palette[p+1] end pixels[idx].face=t.face pixels[idx].cullchk[t.parent][culled]=culled end -- OPTIMIZATION HELL NEVER ENDS else -- All that's really needed is -- this 'else' clause. if pixels[idx]==nil then pixels[idx]={} end table.insert(pixels[idx],t.color or palette[p+1]) end end end end rect(xyxy[1],xyxy[2],xyxy[3]-xyxy[1],xyxy[4]-xyxy[2],CLEAR) end end -- Works better with smaller triangles function avg(t,k) return (t[1][k]+t[2][k]+t[3][k])/3 end -- Z-sort function sort(a,b) ax=avg(a,'x') ay=avg(a,'y') az=avg(a,'z') bx=avg(b,'x') by=avg(b,'y') bz=avg(b,'z') if az==bz then if ax==bx then return ay>by end return axbz end -- Use combine() before calling if you -- have overlapping or nested objects -- and want them to draw properly. function drawShape(s,shaded) table.sort(s,sort) for i,t in ipairs(s) do drawTriangle(t,shaded) end end -- Generates a line of 2-triangle -- rectangles, each with a texture -- pointing to a mapped ASCII char -- specified in the tiles editor. -- 'bg' is the knockout palette index -- for transparent backgrounds. function text(letters,bg) local shape={ id=ID } ID=ID+1 local len=string.len(letters) local x=-len/2 for n=1,len do local byte=string.byte(letters,n) local col=byte//32 local ty=73+7*col -- Cheater's ASCII table: don't -- support the first char of -- every set or the last 6. Good -- thing I don't need them. local tx=(byte-1-col*32)*5 -- For wide chars local adj=0 if tx==60 or tx==110 then adj=.2 end -- Wide M if tx>64 then tx=tx+1 end -- Wide W if tx>115 then tx=tx+1 end if byte==32 then adj=-.5 else --5x7 chars table.insert(shape,{ {x=x+1,y=.7,z=0}, {x=x,y=.7,z=0}, {x=x,y=-.7,z=0}, parent=shape.id, texture={ {x=tx+5,y=ty+6}, {x=tx,y=ty+6}, {x=tx,y=ty}, bg=bg or -1 } }) table.insert(shape,{ {x=x+1,y=.7,z=0}, {x=x+1,y=-.7,z=0}, {x=x,y=-.7,z=0}, parent=shape.id, texture={ {x=tx+5,y=ty+6}, {x=tx+5,y=ty}, {x=tx,y=ty}, bg=bg or -1 } }) end x=x+1+adj end -- Invert rects for expected behavior scale(shape,-1,-1,1) return shape end function recolor(i,r,g,b) if i>=0 or i<16 then local i=math.min(15,math.max(0,i)) local r=math.min(255,math.max(0,r)) local g=math.min(255,math.max(0,g)) local b=math.min(255,math.max(0,b)) poke(0x3fc0+(i*3),r) poke(0x3fc0+(i*3)+1,g) poke(0x3fc0+(i*3)+2,b) end end -- Prepmultiply alpha with a solid bg -- color and a transparent fg color. -- Returns the resulting solid color. function premult(bgr,bgg,bgb,fgr,fgg,fgb,alpha) local pr=(1-alpha)*fgr+alpha*bgr local pg=(1-alpha)*fgg+alpha*bgg local pb=(1-alpha)*fgb+alpha*bgb return ({pr,pg,pb}) end function cachepalette() palette={} for i=0,15 do palette[i+1]={ peek(0x03FC0+i*3), peek(0x03FC0+i*3+1), peek(0x03FC0+i*3+2), 0 } end end function drawPalette() for i=0,7 do rect(2+i*5,124,5,5,i) end for i=0,7 do rect(2+i*5,129,5,5,i+8) end end function precalc() overscan={ palette={}, palettemap={} } for y=pixels.miny,pixels.maxy do local colormap={} local colorindex=0 for x=0,239 do local idx=y*240+x if pixels[idx]~=nil then local color=palette[CLEAR+1] -- Top is opaque; skip premult if pixels[idx][#pixels[idx]][4]==0 then color=pixels[idx][#pixels[idx]] else for i,p in ipairs(pixels[idx]) do -- Opaque; ignore the previous if p[4]==0 then color=p else color=premult( color[1], color[2], color[3], p[1], p[2], p[3], p[4] ) end end end -- Using '..' is a bit slower local ckey=color[1]+(color[2]*1000)+(color[3]*10000) local index=colormap[ckey] local overscanindex=overscan.palettemap[ckey] if index==nil then if colorindex==CLEAR then if bdrpalettes[y]==nil then bdrpalettes[y]={} end local bg=palette[CLEAR+1] bdrpalettes[y][colorindex]=bg colorindex=colorindex+1 end colormap[ckey]=colorindex index=colorindex if index<16 then if bdrpalettes[y]==nil then bdrpalettes[y]={} end bdrpalettes[y][colorindex]=color elseif overscanindex==nil then table.insert(overscan.palette,color) overscanindex=#overscan.palette overscan.palettemap[ckey]=overscanindex end colorindex=colorindex+1 end if index<16 then pix(x,y,index) else table.insert(overscan,{x,y,overscanindex}) end end if DEBUG and x<16 then pix(x,y,x) end end end end FPMS=1000/60 DEBUG=false FPS=true -- Turning this off can improve frame -- rates but you may see extra colors -- where two planes meet but don't -- intersect such as corners and edges -- of triangles on faces composed of -- 2+ triangles due to the projection -- from 3 to 2 dimensions. AVOID_DOUBLE_DRAW=true fps=-1 m=-1 CLEAR=4 function BOOT() bdrpalettes={} cachepalette() poke(0x03FF8,4) end function TIC() t=time() if m<0 then m=t end local elapsed=t-m cls(CLEAR) pixels={ miny=math.huge, maxy=-math.huge } local alpha=0 local a2=.45 if elapsed>5000 then alpha=math.min(.45,(elapsed-5000)/1000*.45) end if elapsed>30000 then alpha=math.min(1,.45+(elapsed-30000)/1000*.55) end if elapsed>35000 then a2=math.min(1,.45+(elapsed-35000)/1000*.55) end local r=elapsed/50 local b0=copy(ic,alpha) scale(b0,2,2,2) rotate(b0,r,r,r) translate(b0,c.x,c.y,10) local b7=copy(b4t,a2) rotate(b7,-r,-r,-r) translate(b7,c.x,c.y,10) local tr=r/10 local str=' HAPPY NEW YEAR AND HELLO 2024! GREETINGS TO T'..string.char(244)..'BACH, RACOONVIOLET, NICO, ALDROID, REALITY404, LYNN DRUMM, PURPLE, MANTATRONIC, JTRUK, TRUCK, & SO MANY MORE! '..string.char(1)..string.char(2)..string.char(3)..string.char(4) local ltr=nil for i=1,#str do local t=text(string.sub(str,i,i),0) local where=tr-i*.30 local rr=where/(2*math.pi)*360+90 if rr>-200 and rr<200 then rotate(t,0,rr,0) local x=5*math.cos(where)+c.x local y=2*math.cos(where)+c.y local z=5*math.sin(where)+10 translate(t,x,y,z) if ltr==nil then ltr=t else ltr=combine(ltr,t) end end end local s10=combine(b0,b7) if ltr then s10=combine(s10,ltr) end drawShape(s10) -- MUST be called after all functions -- that write to the pixels map! precalc() if DEBUG then print(tostring(r),2,2,1) drawPalette() end if DEBUG or FPS then print('fps ',196,2,12) print(math.floor(1000/(t-fps)*10)/10,220,2,12) fps=t end end function BDR(s) if s==0 then cachepalette() end local y=s-4 if bdrpalettes[y]~=nil then for i,color in pairs(bdrpalettes[y]) do recolor(i,color[1],color[2],color[3]) end end -- Restore palette for the next pass. -- Texture-painted triangles, print() -- text, etc. will get very flashy if -- we don't. if s==143 then for i=0,15 do recolor(i,palette[i+1][1],palette[i+1][2],palette[i+1][3]) end end end -- Optimization to paint pixels when -- we run out of colors in BDR. Fixes -- small problems but complex overlaps -- can still blow past this buffer. function OVR() if overscan~=nil and #overscan>0 then for i,c in ipairs(overscan.palette) do recolor(i,c[1],c[2],c[3]) end for i,p in ipairs(overscan) do pix(p[1],p[2],p[3]) end end if DEBUG then drawPalette() end end -- The virtual camera position x/y -- z is the backplane depth c={x=120,y=68,z=100} -- 24 triangle cube -- Seems to render better with avg() -- sorting than a 12 triangle cube. -- Could probably be generated to save -- space in the file. b4t={ -- bottom { {x=0,y=-1,z=0}, {x=-1,y=-1,z=-1}, {x=-1,y=-1,z=1}, face=1, color={107,110,207,.45} }, { {x=-1,y=-1,z=-1}, {x=0,y=-1,z=0}, {x=1,y=-1,z=-1}, face=1, color={107,110,207,.45} }, { {x=1,y=-1,z=-1}, {x=0,y=-1,z=0}, {x=1,y=-1,z=1}, face=1, color={107,110,207,.45} }, { {x=0,y=-1,z=0}, {x=-1,y=-1,z=1}, {x=1,y=-1,z=1}, face=1, color={107,110,207,.45} }, -- top { {x=-1,y=1,z=-1}, {x=0,y=1,z=0}, {x=-1,y=1,z=1}, face=2, color={181,207,107,.45} }, { {x=0,y=1,z=0}, {x=-1,y=1,z=-1}, {x=1,y=1,z=-1}, face=2, color={181,207,107,.45} }, { {x=0,y=1,z=0}, {x=1,y=1,z=-1}, {x=1,y=1,z=1}, face=2, color={181,207,107,.45} }, { {x=-1,y=1,z=1}, {x=0,y=1,z=0}, {x=1,y=1,z=1}, face=2, color={181,207,107,.45} }, -- back { {x=0,y=0,z=1}, {x=1,y=1,z=1}, {x=1,y=-1,z=1}, face=3, color={231,186,82,.45} }, { {x=0,y=0,z=1}, {x=1,y=-1,z=1}, {x=-1,y=-1,z=1}, face=3, color={231,186,82,.45} }, { {x=0,y=0,z=1}, {x=-1,y=-1,z=1}, {x=-1,y=1,z=1}, face=3, color={231,186,82,.45} }, { {x=0,y=0,z=1}, {x=-1,y=1,z=1}, {x=1,y=1,z=1}, face=3, color={231,186,82,.45} }, -- front { {x=1,y=1,z=-1}, {x=0,y=0,z=-1}, {x=1,y=-1,z=-1}, face=4, color={214,97,107,.45} }, { {x=1,y=-1,z=-1}, {x=0,y=0,z=-1}, {x=-1,y=-1,z=-1}, face=4, color={214,97,107,.45} }, { {x=-1,y=-1,z=-1}, {x=0,y=0,z=-1}, {x=-1,y=1,z=-1}, face=4, color={214,97,107,.45} }, { {x=-1,y=1,z=-1}, {x=0,y=0,z=-1}, {x=1,y=1,z=-1}, face=4, color={214,97,107,.45} }, -- right { {x=-1,y=0,z=0}, {x=-1,y=1,z=-1}, {x=-1,y=1,z=1}, face=5, color={206,109,189,.45} }, { {x=-1,y=1,z=-1}, {x=-1,y=0,z=0}, {x=-1,y=-1,z=-1}, face=5, color={206,109,189,.45} }, { {x=-1,y=-1,z=-1}, {x=-1,y=0,z=0}, {x=-1,y=-1,z=1}, face=5, color={206,109,189,.45} }, { {x=-1,y=0,z=0}, {x=-1,y=1,z=1}, {x=-1,y=-1,z=1}, face=5, color={206,109,189,.45} }, -- left { {x=1,y=1,z=-1}, {x=1,y=0,z=0}, {x=1,y=1,z=1}, face=6, color={23,190,207,.45} }, { {x=1,y=0,z=0}, {x=1,y=1,z=-1}, {x=1,y=-1,z=-1}, face=6, color={23,190,207,.45} }, { {x=1,y=0,z=0}, {x=1,y=-1,z=-1}, {x=1,y=-1,z=1}, face=6, color={23,190,207,.45} }, { {x=1,y=1,z=1}, {x=1,y=0,z=0}, {x=1,y=-1,z=1}, face=6, color={23,190,207,.45} } } -- icosahedron (D20) ic=(function() local g=1.618 local fb={ tf={x=0,y=-1,z=g}, tb={x=0,y=-1,z=-g}, bb={x=0,y=1,z=-g}, bf={x=0,y=1,z=g} } local lr={ fr={x=-g,y=0,z=1}, br={x=-g,y=0,z=-1}, bl={x=g,y=0,z=-1}, fl={x=g,y=0,z=1} } local tb={ tr={x=-1,y=-g,z=0}, br={x=-1,y=g,z=0}, bl={x=1,y=g,z=0}, tl={x=1,y=-g,z=0} } -- fb -> vertical plane, front to back -- lr -> flat plane, left to right -- tb -> vertical plane, top to bottom local shape={ newt(tb.tr,fb.tf,tb.tl,{57,59,121,.45}), newt(lr.fr,fb.tf,tb.tr,{82,84,163,.45}), newt(lr.fl,tb.tl,fb.tf,{107,110,207,.45}), newt(fb.bf,fb.tf,lr.fr,{156,158,222,.45}), newt(fb.tf,fb.bf,lr.fl,{99,121,57,.45}), newt(tb.bl,lr.fl,fb.bf,{140,162,82,.45}), newt(tb.bl,fb.bf,tb.br,{181,207,107,.45}), newt(tb.br,fb.bf,lr.fr,{206,219,156,.45}), newt(lr.fl,tb.bl,lr.bl,{140,109,49,.45}), newt(tb.bl,fb.bb,lr.bl,{189,158,57,.45}), newt(fb.bb,tb.bl,tb.br,{231,186,82,.45}), newt(fb.bb,tb.br,lr.br,{231,203,148,.45}), newt(lr.fr,lr.br,tb.br,{132,60,57,.45}), newt(fb.bb,fb.tb,lr.bl,{173,73,74,.45}), newt(fb.tb,fb.bb,lr.br,{214,97,107,.45}), newt(tb.tl,lr.bl,fb.tb,{231,150,156,.45}), newt(tb.tr,tb.tl,fb.tb,{123,65,115,.45}), newt(lr.br,tb.tr,fb.tb,{165,81,148,.45}), newt(tb.tr,lr.br,lr.fr,{206,109,189,.45}), newt(lr.bl,tb.tl,lr.fl,{222,158,214,.45}) } return shape end)()