├── .vscode └── settings.json ├── LICENSE ├── Mountains.lua ├── Pine3D-minified.lua ├── Pine3D.lua ├── README.md ├── betterblittle.lua ├── converter ├── bitmap.lua ├── bmpConverter.lua ├── objConverter.lua └── objLoader.lua ├── models ├── box ├── emerald ├── pineapple └── pinetree └── noise.lua /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Lua.workspace.userThirdParty": [ 3 | "C:\\Users\\Xelo\\Documents\\lua_environments" 4 | ], 5 | "Lua.diagnostics.globals": [ 6 | "colors", 7 | "term", 8 | "window", 9 | "bit", 10 | "textutils", 11 | "fs", 12 | "sleep" 13 | ] 14 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Xella 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Mountains.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Made by Xella#8655 3 | 4 | local Pine3D = require("Pine3D") 5 | local noise = require("noise") 6 | 7 | local genOptions = { 8 | seed = os.clock(), 9 | chunkRows = 2, 10 | chunkColumns = 2, 11 | maxHeight = 32, 12 | relativeSnowHeight = 0.70, 13 | relativeWaterHeight = 0.4, 14 | noiseSize = 32, 15 | terrainSmoothness = 3, 16 | } 17 | 18 | local speed = 6 19 | local turnSpeed = 180 20 | 21 | local camera = { 22 | x = 0, 23 | y = 0, 24 | z = 0, 25 | rotX = 0, 26 | rotY = 0, 27 | rotZ = 0, 28 | } 29 | 30 | local ThreeDFrame = Pine3D.newFrame() 31 | 32 | local function generateWorld(ThreeDFrame, genOptions) 33 | local seed = genOptions.seed 34 | local rows = genOptions.chunkRows 35 | local columns = genOptions.chunkColumns 36 | local maxHeight = genOptions.maxHeight 37 | local relativeSnowHeight = genOptions.relativeSnowHeight 38 | local relativeWaterHeight = genOptions.relativeWaterHeight 39 | local noiseSize = genOptions.noiseSize 40 | local terrainSmoothness = genOptions.terrainSmoothness 41 | 42 | local polyCount = 0 43 | local objects = {} 44 | 45 | local colors = colors 46 | local sqrt = math.sqrt 47 | local max = math.max 48 | 49 | math.randomseed(seed) 50 | for chunkX = 0, columns - 1 do 51 | for chunkZ = 0, rows - 1 do 52 | local mapNoise1 = noise.createNoise(noiseSize, chunkX, chunkZ, seed, terrainSmoothness) 53 | local mapNoise2 = noise.createNoise(noiseSize, chunkX + 1, chunkZ, seed, terrainSmoothness) 54 | local mapNoise3 = noise.createNoise(noiseSize, chunkX, chunkZ + 1, seed, terrainSmoothness) 55 | local mapNoise4 = noise.createNoise(noiseSize, chunkX + 1, chunkZ + 1, seed, terrainSmoothness) 56 | 57 | local mapA = {} 58 | local mapB = {} 59 | for a = 1, noiseSize do 60 | for b = 1, noiseSize do 61 | local height1 = mapNoise1[a][b]*maxHeight - 0.5*maxHeight 62 | local height2 = 0 63 | if (a < noiseSize) then 64 | height2 = mapNoise1[a+1][b]*maxHeight - 0.5*maxHeight 65 | else 66 | height2 = mapNoise2[1][b]*maxHeight - 0.5*maxHeight 67 | end 68 | local height3 = 0 69 | if (b < noiseSize) then 70 | height3 = mapNoise1[a][b+1]*maxHeight - 0.5*maxHeight 71 | else 72 | height3 = mapNoise3[a][1]*maxHeight - 0.5*maxHeight 73 | end 74 | local height4 = 0 75 | if (a == noiseSize and b == noiseSize) then 76 | height4 = mapNoise4[1][1]*maxHeight - 0.5*maxHeight 77 | elseif (a == noiseSize) then 78 | height4 = mapNoise2[1][b+1]*maxHeight - 0.5*maxHeight 79 | elseif (b == noiseSize) then 80 | height4 = mapNoise3[a+1][1]*maxHeight - 0.5*maxHeight 81 | else 82 | height4 = mapNoise1[a+1][b+1]*maxHeight - 0.5*maxHeight 83 | end 84 | 85 | local c1 = colors.lime 86 | local c2 = colors.green 87 | 88 | local snowHeight = relativeSnowHeight * maxHeight - 0.5*maxHeight 89 | local waterHeight = relativeWaterHeight * maxHeight - 0.5*maxHeight 90 | 91 | if (height1 >= snowHeight or height2 >= snowHeight or height3 >= snowHeight) then 92 | c1 = colors.white 93 | end 94 | if (height2 >= snowHeight or height3 >= snowHeight or height4 >= snowHeight) then 95 | c2 = colors.lightGray 96 | end 97 | if (height1 <= waterHeight or height2 <= waterHeight or height3 <= waterHeight or height4 <= waterHeight) then 98 | height1 = max(height1, waterHeight) 99 | height2 = max(height2, waterHeight) 100 | height3 = max(height3, waterHeight) 101 | height4 = max(height4, waterHeight) 102 | if (height1 <= waterHeight and height2 <= waterHeight and height3 <= waterHeight) then 103 | c1 = colors.blue 104 | end 105 | if (height2 <= waterHeight and height3 <= waterHeight and height4 <= waterHeight) then 106 | c2 = colors.blue 107 | end 108 | end 109 | 110 | local map = mapA 111 | if b + a > noiseSize+1 then 112 | map = mapB 113 | end 114 | 115 | local xOffset = 0 116 | local zOffset = 0 117 | if map == mapA then 118 | xOffset = -(1/2 + 1) + -noiseSize*0.5 119 | zOffset = -sqrt(0.75) + -noiseSize * sqrt(0.75) / 3 120 | else 121 | xOffset = -(1/2 + 1) + -noiseSize 122 | zOffset = -sqrt(0.75) + -noiseSize * sqrt(0.75) * 2 / 3 123 | end 124 | 125 | map[#map+1] = { 126 | x1 = xOffset + b/2 + a+1, y1 = height2, z1 = zOffset + b*sqrt(0.75), 127 | x2 = xOffset + b/2 + a, y2 = height1, z2 = zOffset + b*sqrt(0.75), 128 | x3 = xOffset + b/2 + a+0.5, y3 = height3, z3 = zOffset + (b+1)*sqrt(0.75), 129 | c = c1, 130 | } 131 | 132 | if b + a == noiseSize+1 then 133 | map = mapB 134 | 135 | xOffset = -(1/2 + 1) + -noiseSize 136 | zOffset = -sqrt(0.75) + -noiseSize * sqrt(0.75) * 2 / 3 137 | end 138 | 139 | map[#map+1] = { 140 | x1 = xOffset + b/2 + a+0.5, y1 = height3, z1 = zOffset + (b+1)*sqrt(0.75), 141 | x2 = xOffset + b/2 + a+1.5, y2 = height4, z2 = zOffset + (b+1)*sqrt(0.75), 142 | x3 = xOffset + b/2 + a+1, y3 = height2, z3 = zOffset + b*sqrt(0.75), 143 | c = c2, 144 | } 145 | 146 | polyCount = polyCount + 2 147 | end 148 | end 149 | 150 | objects[#objects+1] = ThreeDFrame:newObject( 151 | mapA, -- model 152 | chunkX * noiseSize + chunkZ * noiseSize*0.5, -- X 153 | 0, -- Y 154 | chunkZ * noiseSize * sqrt(0.75) -- Z 155 | ) 156 | 157 | objects[#objects+1] = ThreeDFrame:newObject( 158 | mapB, -- model 159 | chunkX * noiseSize + chunkZ * noiseSize*0.5 + noiseSize*0.5, -- X 160 | 0, -- Y 161 | chunkZ * noiseSize * sqrt(0.75) + noiseSize * sqrt(0.75)/3 -- Z 162 | ) 163 | end 164 | end 165 | 166 | print("polyCount: " .. polyCount) 167 | sleep(0.5) 168 | 169 | return objects 170 | end 171 | 172 | local objects = generateWorld(ThreeDFrame, genOptions) 173 | 174 | local function rendering() 175 | local frames = 0 176 | local lastFPSTime = 0 177 | while true do 178 | ThreeDFrame:drawObjects(objects) 179 | ThreeDFrame:drawBuffer() 180 | 181 | --[[frames = frames + 1 182 | if os.clock() > lastFPSTime + 1 then 183 | lastFPSTime = os.clock() 184 | term.setBackgroundColor(colors.black) 185 | term.setCursorPos(1, 1) 186 | term.clearLine() 187 | term.setCursorPos(1, 1) 188 | term.setTextColor(colors.white) 189 | term.write("Average FPS: " .. frames) 190 | frames = 0 191 | end]]-- 192 | 193 | os.queueEvent("FakeEvent") 194 | os.pullEvent("FakeEvent") 195 | end 196 | end 197 | 198 | local keysDown = {} 199 | local function keyInput() 200 | while true do 201 | local event, key = os.pullEventRaw() 202 | 203 | if event == "key" then 204 | keysDown[key] = true 205 | elseif event == "key_up" then 206 | keysDown[key] = nil 207 | end 208 | end 209 | end 210 | 211 | local function handleCameraMovement(dt) 212 | local dx, dy, dz = 0, 0, 0 -- will represent the movement per second 213 | 214 | -- handle arrow keys for camera rotation 215 | if keysDown[keys.left] then 216 | camera.rotY = (camera.rotY - turnSpeed * dt) % 360 217 | end 218 | if keysDown[keys.right] then 219 | camera.rotY = (camera.rotY + turnSpeed * dt) % 360 220 | end 221 | if keysDown[keys.down] then 222 | camera.rotZ = math.max(-80, camera.rotZ - turnSpeed * dt) 223 | end 224 | if keysDown[keys.up] then 225 | camera.rotZ = math.min(80, camera.rotZ + turnSpeed * dt) 226 | end 227 | 228 | -- handle wasd keys for camera movement 229 | if keysDown[keys.w] then 230 | dx = speed * math.cos(math.rad(camera.rotY)) + dx 231 | dz = speed * math.sin(math.rad(camera.rotY)) + dz 232 | end 233 | if keysDown[keys.s] then 234 | dx = -speed * math.cos(math.rad(camera.rotY)) + dx 235 | dz = -speed * math.sin(math.rad(camera.rotY)) + dz 236 | end 237 | if keysDown[keys.a] then 238 | dx = speed * math.cos(math.rad(camera.rotY - 90)) + dx 239 | dz = speed * math.sin(math.rad(camera.rotY - 90)) + dz 240 | end 241 | if keysDown[keys.d] then 242 | dx = speed * math.cos(math.rad(camera.rotY + 90)) + dx 243 | dz = speed * math.sin(math.rad(camera.rotY + 90)) + dz 244 | end 245 | 246 | -- space and left shift key for moving the camera up and down 247 | if keysDown[keys.space] then 248 | dy = speed + dy 249 | end 250 | if keysDown[keys.leftShift] then 251 | dy = -speed + dy 252 | end 253 | 254 | -- update the camera position by adding the offset 255 | camera.x = camera.x + dx * dt 256 | camera.y = camera.y + dy * dt 257 | camera.z = camera.z + dz * dt 258 | 259 | ThreeDFrame:setCamera(camera) 260 | end 261 | 262 | local function gameLoop() 263 | local lastTime = os.clock() 264 | 265 | while true do 266 | -- compute the time passed since last step 267 | local currentTime = os.clock() 268 | local dt = currentTime - lastTime 269 | lastTime = currentTime 270 | 271 | -- run all functions that need to be run 272 | handleCameraMovement(dt) 273 | 274 | -- use a fake event to yield the coroutine 275 | os.queueEvent("gameLoop") 276 | os.pullEventRaw("gameLoop") 277 | end 278 | end 279 | 280 | parallel.waitForAll(keyInput, gameLoop, rendering) 281 | -------------------------------------------------------------------------------- /Pine3D-minified.lua: -------------------------------------------------------------------------------- 1 | local a=(...):match("(.-)[^%.]+$")local b=require(a.."betterblittle")local c=math.min;local d=math.max;local e=math.floor;local f=math.ceil;local function g(h,i,j,k)local l={x1=h,y1=i,x2=j,y2=k,width=j-h+1,height=k-i+1,screenBuffer={{}},blitWin=window.create(term.current(),h,i,j-h+1,k-i+1,false),backgroundColor=colors.lightBlue}function l:setBufferSize(m,n,o,p)self.x1=m;self.y1=n;self.x2=o;self.y2=p;self.width=o-m+1;self.height=p-n+1;self.blitWin.reposition(self.x1,self.y1,self.width,self.height)self:clear()end;function l:clear()local q=self.screenBuffer;q.c2={}q.depth={}local r=q.c2;local s=q.depth;local t=math.huge;local u=self.width;local v=self.backgroundColor;for w=1,self.height do r[w]={}s[w]={}local x=r[w]local y=s[w]for z=1,u do x[z]=v;y[z]=t end end end;function l:clearDepth()local q=self.screenBuffer;local s=q.depth;local t=-math.huge;local u=self.width;for w=1,self.height do local y=s[w]for z=1,u do y[z]=t end end end;function l:fastClear()local A=self.backgroundColor;local r=self.screenBuffer.c2;local u=self.width;for w=1,self.height do local x=r[w]for z=1,u do x[z]=A end end end;function l:setPixel(z,w,A)z=e(z+0.5)w=e(w+0.5)if z>=1 and z<=self.width then if w>=1 and w<=self.height then local q=self.screenBuffer;q.c2[w][z]=A end end end;function l:image(z,w,B,C,D)local q=self.screenBuffer;local u=self.width;local E=(e(z+0.5)-1)*2+(C or 0)local F=(e(w+0.5)-1)*3+(D or 0)local r=q.c2;for G,H in pairs(B)do local I=G+F;local x=r[I]if x then for J,K in pairs(H)do if K and K>0 then local L=J+E;if L>=1 and L<=u then x[L]=K end end end end end end;function l:loadLineNoInterp(h,i,j,k,A,M)local q=self.screenBuffer;local r=q.c2;local N=q.depth;local O=self.width;local P=self.height;local E=j-h;local F=k-i;if E~=0 then for z=d(f(c(h,j)),1),c(e(d(h,j)),O)do local Q=(z-h)/E;local w=e(i+Q*F+0.5)if w>0 and w<=P and M>=N[w][z]then r[w][z]=A;N[w][z]=M end end end;if F~=0 then for w=d(f(c(i,k)),1),c(e(d(i,k)),P)do local R=(w-i)/F;local z=e(h+R*E+0.5)if z>0 and z<=O and M>=N[w][z]then r[w][z]=A;N[w][z]=M end end end end;function l:loadLineInterp(h,i,j,k,A,S,T)local q=self.screenBuffer;local r=q.c2;local N=q.depth;local O=self.width;local P=self.height;h=h+0.5;j=j+0.5;i=i-0.5;k=k-0.5;local E=j-h;local F=k-i;local U=T-S;if E~=0 then for z=d(f(c(h,j)),1),c(e(d(h,j)),O)do local Q=(z-h)/E;local w=e(i+Q*F+0.5)local V=S+Q*U;if w>0 and w<=P and V>=N[w][z]then r[w][z]=A;N[w][z]=V end end end;if F~=0 then for w=d(f(c(i,k)),1),c(e(d(i,k)),P)do local R=(w-i)/F;local z=e(h+R*E+0.5)local V=S+R*U;if z>0 and z<=O and V>=N[w][z]then r[w][z]=A;N[w][z]=V end end end end;local W=colors.black;function l:drawTriangleNoInterp(h,i,j,k,X,Y,A,Z,s)if h<0 and j<0 and X<0 or i<1 and k<1 and Y<1 then return end;local O=self.width;if h>O and j>O and X>O then return end;local P=self.height;if i>P and k>P and Y>P then return end;if i>k then i,k=k,i;h,j=j,h end;if k>Y then Y,k=k,Y;X,j=j,X end;if i>k then i,k=k,i;h,j=j,h end;local q=self.screenBuffer;local e,f=e,f;local c,d=c,d;local _=c(d(1,f(i)),P)local a0=c(d(0,e(k)),P)local a1=c(d(1,e(Y)),P)local r=q.c2;local N=q.depth;local M=1/s;if i~=k and i~=Y then local a2=(j-h)/(k-i)local a3=(X-h)/(Y-i)for w=_,a0 do local F=w-i;local a4=h+F*a2;local a5=h+F*a3;if a5O then a7=O end;local x=r[w]local y=N[w]for z=a6,a7 do if M>y[z]then y[z]=M;x[z]=A end end end end;if Y~=k and i~=Y then local a2=(j-X)/(k-Y)local a3=(h-X)/(i-Y)for w=a0+1,a1 do local F=w-Y;local a4=X+F*a2;local a5=X+F*a3;if a5O then a7=O end;local x=r[w]local y=N[w]for z=a6,a7 do if M>y[z]then y[z]=M;x[z]=A end end end end;local Z=Z;if Z or self.triangleEdges then local a8=self.loadLineNoInterp;local A=Z or W;a8(self,h,i,j,k,A,M)a8(self,j,k,X,Y,A,M)a8(self,X,Y,h,i,A,M)end end;function l:drawTriangleInterp(h,i,j,k,X,Y,A,Z,a9,aa,ab,ac)if h<0 and j<0 and X<0 or i<1 and k<1 and Y<1 then return end;local O=self.width;if h>O and j>O and X>O then return end;local P=self.height;if i>P and k>P and Y>P then return end;if i>k then i,k=k,i;h,j=j,h;aa,ab=ab,aa end;if k>Y then Y,k=k,Y;X,j=j,X;ac,ab=ab,ac end;if i>k then i,k=k,i;h,j=j,h;aa,ab=ab,aa end;local q=self.screenBuffer;local e,f=e,f;local c,d=c,d;local _=c(d(1,f(i)),P)local a0=c(d(0,e(k)),P)local a1=c(d(1,e(Y)),P)local r=q.c2;local N=q.depth;local ad=1/ac;local T=1/ab;local S=1/aa;if i~=k and i~=Y then local ae=k-i;local af=Y-i;local a2=(j-h)/ae;local a3=(X-h)/af;local ag=T-S;local ah=ad-S;for w=_,a0 do local F=w-i;local a4=h+F*a2;local a5=h+F*a3;local ai=F/ae;local aj=F/af;local ak=S+ai*ag;local al=S+aj*ah;if a5O then a7=O end;a4=e(a4+0.5)a5=e(a5+1.5)local am=a5-a4;local x=r[w]local y=N[w]local an=al-ak;for z=a6,a7 do local Q=(z-a4)/am;local V=ak+Q*an;if V>y[z]then y[z]=V;x[z]=A end end end end;if Y~=k and i~=Y then local ao=k-Y;local ap=i-Y;local a2=(j-X)/ao;local a3=(h-X)/ap;local ag=T-ad;local ah=S-ad;for w=a0+1,a1 do local F=w-Y;local a4=X+F*a2;local a5=X+F*a3;local ai=F/ao;local aj=F/ap;local ak=ad+ai*ag;local al=ad+aj*ah;if a5O then a7=O end;a4=e(a4+0.5)a5=e(a5+1.5)local am=a5-a4;local x=r[w]local y=N[w]local an=al-ak;for z=a6,a7 do local Q=(z-a4)/am;local V=ak+Q*an;if V>y[z]then y[z]=V;x[z]=A end end end end;local Z=Z;if Z or self.triangleEdges then local a8=self.loadLineInterp;local A=Z or W;a8(self,h,i,j,k,A,S,T)a8(self,j,k,X,Y,A,T,ad)a8(self,X,Y,h,i,A,ad,S)end end;function l:drawBuffer()local aq=self.blitWin;b.drawBuffer(self.screenBuffer.c2,aq)aq.setVisible(true)aq.setVisible(false)end;function l:depthInterpolation(ar)self.drawTriangle=ar and self.drawTriangleInterp or self.drawTriangleNoInterp;self:clear()end;function l:useTriangleEdges(ar)self.triangleEdges=ar end;l:depthInterpolation(true)return l end;local function as(at,au,av,aw,ax)local ay=ax[1]local az=ax[2]local aA=ax[3]local aB=au and au-ay or 0;local aC=av and av-az or 0;local aD=aw and aw-aA or 0;for aE=1,#at do local aF=at[aE]local aG=aB+(aF[1]+aF[4]+aF[7])/3;local aH=aC+(aF[2]+aF[5]+aF[8])/3;local aI=aD+(aF[3]+aF[6]+aF[9])/3;aF[16]=aG*aG+aH*aH+aI*aI end end;local aJ=math.rad;local aK=math.sin;local aL=math.cos;local function aM(z,w,aN,aO,aP)local ab=aP*aN-aO*w;local w=aO*aN+aP*w;local aN=ab;return z,w,aN end;local function aQ(z,w,aN,aO,aP)local ab=aP*aN-aO*z;local z=aO*aN+aP*z;local aN=ab;return z,w,aN end;local function aR(z,w,aN,aO,aP)local k=aP*w-aO*z;local z=aO*w+aP*z;local w=k;return z,w end;local function aS(aT,aU,aV,aW)local aX,aY=0,1;local aZ,a_=0,1;local b0,b1=0,1;if aU==0 then aU=nil end;if aU then aX,aY=aK(aU),aL(aU)end;if aV==0 then aV=nil end;if aV then aZ,a_=aK(aV),aL(aV)end;if aW==0 then aW=nil end;if aW then b0,b1=aK(aW),aL(aW)end;local b2={}for b3,aF in pairs(aT)do local h,i,aa=aF[1],aF[2],aF[3]local j,k,ab=aF[4],aF[5],aF[6]local X,Y,ac=aF[7],aF[8],aF[9]if aV then h,i,aa=aQ(h,i,aa,aZ,a_)j,k,ab=aQ(j,k,ab,aZ,a_)X,Y,ac=aQ(X,Y,ac,aZ,a_)end;if aW then h,i=aR(h,i,aa,b0,b1)j,k=aR(j,k,ab,b0,b1)X,Y=aR(X,Y,ac,b0,b1)end;if aU then h,i,aa=aM(h,i,aa,aX,aY)j,k,ab=aM(j,k,ab,aX,aY)X,Y,ac=aM(X,Y,ac,aX,aY)end;b2[#b2+1]={h,i,aa,j,k,ab,X,Y,ac}b2[#b2][10]=aF[10]b2[#b2][11]=aF[11]b2[#b2][12]=aF[12]end;return b2 end;local b4={}function b4.invertTriangles(aT)if not aT or type(aT)~="table"then error("transforms.invertTriangles expected arg#1 to be a table (model)")end;for aE=1,#aT do local b5=aT[aE]aT[aE]={x1=b5.x1,y1=b5.y1,z1=b5.z1,x2=b5.x3,y2=b5.y3,z2=b5.z3,x3=b5.x2,y3=b5.y2,z3=b5.z2,c=b5.c,forceRender=b5.forceRender,outlineColor=b5.outlineColor}end;return aT end;function b4.setOutline(aT,b6)if not aT or type(aT)~="table"then error("transforms.invertTriangles expected arg#1 to be a table (model)")end;for aE=1,#aT do local b5=aT[aE]if type(b6)=="table"then b5.outlineColor=b6[b5.c]or b5.outlineColor else b5.outlineColor=b6 end end;return aT end;function b4.mapColor(aT,b6)if not aT or type(aT)~="table"then error("transforms.mapColor expected arg#1 to be a table (model)")end;for aE=1,#aT do local b5=aT[aE]if type(b6)=="table"then b5.c=b6[b5.c]or b5.c else b5.c=b6 end end;return aT end;function b4.center(aT)local b7,b8=math.huge,-math.huge;local _,a1=math.huge,-math.huge;local b9,ba=math.huge,-math.huge;for aE=1,#aT do local bb=aT[aE]b7,b8=c(b7,bb.x1),d(b8,bb.x1)b7,b8=c(b7,bb.x2),d(b8,bb.x2)b7,b8=c(b7,bb.x3),d(b8,bb.x3)_,a1=c(_,bb.y1),d(a1,bb.y1)_,a1=c(_,bb.y2),d(a1,bb.y2)_,a1=c(_,bb.y3),d(a1,bb.y3)b9,ba=c(b9,bb.z1),d(ba,bb.z1)b9,ba=c(b9,bb.z2),d(ba,bb.z2)b9,ba=c(b9,bb.z3),d(ba,bb.z3)end;local bc=-(b8+b7)*0.5;local bd=-(a1+_)*0.5;local be=-(ba+b9)*0.5;for aE=1,#aT do local bb=aT[aE]bb.x1=bb.x1+bc;bb.x2=bb.x2+bc;bb.x3=bb.x3+bc;bb.y1=bb.y1+bd;bb.y2=bb.y2+bd;bb.y3=bb.y3+bd;bb.z1=bb.z1+be;bb.z2=bb.z2+be;bb.z3=bb.z3+be end;return aT,bc,bd,be end;function b4.normalizeScale(aT)local bf=-math.huge;for aE=1,#aT do local bb=aT[aE]bf=d(bf,bb.x1)bf=d(bf,bb.x2)bf=d(bf,bb.x3)bf=d(bf,bb.y1)bf=d(bf,bb.y2)bf=d(bf,bb.y3)bf=d(bf,bb.z1)bf=d(bf,bb.z2)bf=d(bf,bb.z3)end;for aE=1,#aT do local bb=aT[aE]bb.x1=bb.x1/bf;bb.x2=bb.x2/bf;bb.x3=bb.x3/bf;bb.y1=bb.y1/bf;bb.y2=bb.y2/bf;bb.y3=bb.y3/bf;bb.z1=bb.z1/bf;bb.z2=bb.z2/bf;bb.z3=bb.z3/bf end;return aT end;function b4.normalizeScaleY(aT)local bf=-math.huge;for aE=1,#aT do local bb=aT[aE]bf=d(bf,bb.y1)bf=d(bf,bb.y2)bf=d(bf,bb.y3)end;for aE=1,#aT do local bb=aT[aE]bb.x1=bb.x1/bf;bb.x2=bb.x2/bf;bb.x3=bb.x3/bf;bb.y1=bb.y1/bf;bb.y2=bb.y2/bf;bb.y3=bb.y3/bf;bb.z1=bb.z1/bf;bb.z2=bb.z2/bf;bb.z3=bb.z3/bf end;return aT end;function b4.scale(aT,bg)for aE=1,#aT do local bb=aT[aE]bb.x1=bb.x1*bg;bb.x2=bb.x2*bg;bb.x3=bb.x3*bg;bb.y1=bb.y1*bg;bb.y2=bb.y2*bg;bb.y3=bb.y3*bg;bb.z1=bb.z1*bg;bb.z2=bb.z2*bg;bb.z3=bb.z3*bg end;return aT end;function b4.translate(aT,E,F,bh)for aE=1,#aT do local bb=aT[aE]bb.x1=bb.x1+(E or 0)bb.x2=bb.x2+(E or 0)bb.x3=bb.x3+(E or 0)bb.y1=bb.y1+(F or 0)bb.y2=bb.y2+(F or 0)bb.y3=bb.y3+(F or 0)bb.z1=bb.z1+(bh or 0)bb.z2=bb.z2+(bh or 0)bb.z3=bb.z3+(bh or 0)end;return aT end;function b4.rotate(aT,aU,aV,aW)local aX,aY=0,1;local aZ,a_=0,1;local b0,b1=0,1;if aU==0 then aU=nil end;if aU then aX,aY=aK(aU),aL(aU)end;if aV==0 then aV=nil end;if aV then aZ,a_=aK(aV),aL(aV)end;if aW==0 then aW=nil end;if aW then b0,b1=aK(aW),aL(aW)end;for aE=1,#aT do local aF=aT[aE]local h,i,aa=aF.x1,aF.y1,aF.z1;local j,k,ab=aF.x2,aF.y2,aF.z2;local X,Y,ac=aF.x3,aF.y3,aF.z3;if aV then h,i,aa=aQ(h,i,aa,aZ,a_)j,k,ab=aQ(j,k,ab,aZ,a_)X,Y,ac=aQ(X,Y,ac,aZ,a_)end;if aW then h,i=aR(h,i,aa,b0,b1)j,k=aR(j,k,ab,b0,b1)X,Y=aR(X,Y,ac,b0,b1)end;if aU then h,i,aa=aM(h,i,aa,aX,aY)j,k,ab=aM(j,k,ab,aX,aY)X,Y,ac=aM(X,Y,ac,aX,aY)end;aF.x1,aF.y1,aF.z1=h,i,aa;aF.x2,aF.y2,aF.z2=j,k,ab;aF.x3,aF.y3,aF.z3=X,Y,ac end;return aT end;function b4.alignBottom(aT)local _=math.huge;for aE=1,#aT do local bb=aT[aE]_=c(_,bb.y1)_=c(_,bb.y2)_=c(_,bb.y3)end;for aE=1,#aT do local bb=aT[aE]bb.y1=bb.y1-_;bb.y2=bb.y2-_;bb.y3=bb.y3-_ end;return aT end;function b4.decimate(aT,bi,bj)local bk={}local bl={}local function bm(z,w,aN)for aE=#bk,1,-1 do local bn=bk[aE]if bn[1]==z and bn[2]==w and bn[3]==aN then return aE end end end;for aE=1,#aT do local bb=aT[aE]local bo=bm(bb.x1,bb.y1,bb.z1)if not bo then bk[#bk+1]={bb.x1,bb.y1,bb.z1}bo=#bk end;local bp=bm(bb.x2,bb.y2,bb.z2)if not bp then bk[#bk+1]={bb.x2,bb.y2,bb.z2}bp=#bk end;local bq=bm(bb.x3,bb.y3,bb.z3)if not bq then bk[#bk+1]={bb.x3,bb.y3,bb.z3}bq=#bk end;local b5={bo,bp,bq,bb.c,bb.forceRender}bl[#bl+1]=b5 end;local function br(bs,bt)local E=bs[1]-bt[1]local F=bs[2]-bt[2]local bh=bs[3]-bt[3]return(E*E+F*F+bh*bh)^0.5 end;local bu={}for aE=1,#bl do local b5=bl[aE]local bv=br(bk[b5[1]],bk[b5[2]])local bw=br(bk[b5[2]],bk[b5[3]])local bx=br(bk[b5[1]],bk[b5[3]])bu[#bu+1]={length=bv,vA=b5[1],vB=b5[2]}bu[#bu+1]={length=bw,vA=b5[2],vB=b5[3]}bu[#bu+1]={length=bx,vA=b5[1],vB=b5[3]}end;local function by(bz,bA)local bB=bk[bz]local bC=bk[bA]local bD={(bB[1]+bC[1])*0.5,(bB[2]+bC[2])*0.5,(bB[3]+bC[3])*0.5}bk[#bk+1]=bD;local bE=#bk;for aE=#bl,1,-1 do local bF=bl[aE]if bF[1]==bz or bF[1]==bA or bF[2]==bz or bF[2]==bA or bF[3]==bz or bF[3]==bA then if bF[1]==bz or bF[1]==bA then bF[1]=bE end;if bF[2]==bz or bF[2]==bA then bF[2]=bE end;if bF[3]==bz or bF[3]==bA then bF[3]=bE end;if bF[1]==bF[2]or bF[2]==bF[3]or bF[1]==bF[3]then table.remove(bl,aE)end end end;return bE end;local bG=bi;if bj~="polys"then bG=#aT*bi end;while#bl>bG do local bH=math.huge;local bI;for aE=1,#bu do local bJ=bu[aE]if bJ.lengthch then ch=ci end;if cj>ch then ch=cj end;if ck>ch then ch=ck end end;return cg,ch end;function bR(cl)local cm=fs.open(cl,"r")if not cm then error("Could not find model for an object at path: "..cl)end;local cn=cm.readAll()cm.close()if not cn then error("Failed to parse model for an object at path: "..cl)end;local aT=textutils.unserialise(cn)for bP,bQ in pairs(b4)do aT[bP]=bQ end;return aT end;local co=math.pi;local aK=math.sin;local aL=math.cos;local cp=math.tan;local cf=math.sqrt;local function cq(h,i,j,k)local u,cr=term.getSize()if h and j then u=j-h+1 end;if i and k then cr=k-i+1 end;local h=h or 1;local i=i or 1;local j=j or u-h+1;local k=k or cr-i+1;local cs={camera={0.000001,0.000001,0.000001,nil,0,0},buffer=g(h,i,j,k),x1=h,y1=i,x2=j,y2=k,width=u,height=cr}cs.FoV=90;cs.camera[7]=aJ(cs.FoV)cs.camera[8]=2*math.atan(math.tan(aJ(cs.FoV)*0.5)/(cs.width/cs.height/1.5))cs.t=cp(aJ(cs.FoV/2))*2*0.0001;local ct,cu,cv,cw;function cs:setBackgroundColor(A)local cx=self.buffer;cx.backgroundColor=A;cx:fastClear()end;local function cy()ct=e(cs.width*0.5)+1;cu=e(cs.height*0.5)cv=0.0001*cs.width/cs.t;cw=-0.0001*cs.width/(cs.t*cs.height)*cs.height end;function cs:setSize(h,i,j,k)self.x1=h;self.y1=i;self.x2=j;self.y2=k;self.width=(j-h+1)*2;self.height=(k-i+1)*3;self.buffer:setBufferSize(h,i,h+self.width-1,i+self.height-1)cy()end;function cs:depthInterpolation(ar)self.interpolateDepth=ar;self.buffer:depthInterpolation(ar)end;function cs:map3dTo2d(z,w,aN)local ax=self.camera;local cz=aK(ax[4]or 0)local cA=aL(ax[4]or 0)local cB=aK(-ax[5])local cC=aL(-ax[5])local cD=aK(ax[6])local cE=aL(ax[6])local cF=z-ax[1]local cG=w-ax[2]local cH=aN-ax[3]local cI=cC*cF-cB*cH;cH=cB*cF+cC*cH;cF=cI;local cJ=cE*cG-cD*cF;cF=cD*cG+cE*cF;cG=cJ;if cz~=0 then local cK=cz*cH-cA*cG;cG=cA*cH+cz*cG;cH=cK end;local cL=cH/cF*cv+ct;local cM=cG/cF*cw+cu;return cL,cM,cF>=0.0001 end;function cs:drawObject(ca,ax,cN)local cO=ca[1]local cP=ca[2]local cQ=ca[3]local cz=cN[1]local cA=cN[2]local cB=cN[3]local cC=cN[4]local cD=cN[5]local cE=cN[6]local cR=cO and cO-ax[1]or 0;local cS=cP and cP-ax[2]or 0;local cT=cQ and cQ-ax[3]or 0;local bU=ca[9]if bU then bU:update(ca,ax)end;local aT=ca[7]if#aT<=0 then return end;local bW=ca[8]local cF=cR;local cG=cS;local cH=cT;local cI=cC*cF-cB*cH;local cH=cB*cF+cC*cH;local cF=cI;local cJ=cE*cG-cD*cF;local cF=cD*cG+cE*cF;cG=cJ;if cF<-bW then return true end;local cU=0.5*ax[7]local cV=aK(cU)local cW=aL(cU)if(cF+bW)*cV+(cH+bW)*cW<0 then return true end;if(cF+bW)*cV-(cH-bW)*cW<0 then return true end;local cX=0.5*ax[8]local cV=aK(cX)local cY=aL(cX)if(cF+bW)*cV+(cG+bW)*cY<0 then return true end;if(cF+bW)*cV-(cG-bW)*cY<0 then return true end;local aU=ca[4]local aV=ca[5]local aW=ca[6]if aU and aU~=0 or aV and aV~=0 or aW and aW~=0 then aT=aS(aT,aU,aV,aW)end;as(aT,cO,cP,cQ,ax)local cZ=cR*cR+cS*cS+cT*cT0.00010000001 then local j,k,cI=c_(aF[4]+cR,aF[5]+cS,aF[6]+cT)if cI>0.00010000001 then local X,Y,d2=c_(aF[7]+cR,aF[8]+cS,aF[9]+cT)if d2>0.00010000001 then if aF[10]or(j-h)*(Y-k)-(k-i)*(X-j)<0 then cx:drawTriangle(h,i,j,k,X,Y,aF[11],aF[12],s,d1,cI,d2)end elseif cZ then local function d3(z,w,aN)local cF=z+cR;local cG=w+cS;local cH=aN+cT;local cI=cC*cF-cB*cH;cH=cB*cF+cC*cH;cF=cI;local cJ=cE*cG-cD*cF;cF=cD*cG+cE*cF;cG=cJ;if cz~=0 then local cK=cz*cH-cA*cG;cG=cA*cH+cz*cG;cH=cK end;local cL=cH/cF*cv+ct;local cM=cG/cF*cw+cu;return cL,cM,cF,cG,cH end;local h,i,d1,d4,d5=d3(aF[1],aF[2],aF[3])local j,k,cI,cJ,cK=d3(aF[4],aF[5],aF[6])local X,Y,d2,d6,d7=d3(aF[7],aF[8],aF[9])local d8=math.abs;local d9=d8(d2-0.0001)local da=d8(d1-0.0001)local db=da+d9;local dc=(d7*da+d5*d9)/db;local dd=(d6*da+d4*d9)/db;local ct,cv,cu,cw=ct,cv,cu,cw;local de=dc*10000*cv+ct;local df=dd*10000*cw+cu;if aF[10]or(j-h)*(df-k)-(k-i)*(de-j)<0 then cx:drawTriangle(h,i,j,k,de,df,aF[11],aF[12],s,d1,cI,0.0001)local dg=d8(cI-0.0001)local db=dg+d9;local dc=(cK*d9+d7*dg)/db;local dd=(cJ*d9+d6*dg)/db;local dh=dc*10000*cv+ct;local di=dd*10000*cw+cu;cx:drawTriangle(dh,di,j,k,de,df,aF[11],aF[12],s,0.0001,cI,0.0001)end end elseif cZ then local function d3(z,w,aN)local cF=z+cR;local cG=w+cS;local cH=aN+cT;local cI=cC*cF-cB*cH;cH=cB*cF+cC*cH;cF=cI;local cJ=cE*cG-cD*cF;cF=cD*cG+cE*cF;cG=cJ;if cz~=0 then local cK=cz*cH-cA*cG;cG=cA*cH+cz*cG;cH=cK end;local cL=cH/cF*cv+ct;local cM=cG/cF*cw+cu;return cL,cM,cF,cG,cH end;local h,i,d1,d4,d5=d3(aF[1],aF[2],aF[3])local j,k,cI,cJ,cK=d3(aF[4],aF[5],aF[6])local X,Y,d2,d6,d7=d3(aF[7],aF[8],aF[9])local d8=math.abs;if d2>0.00010000001 then local dg=d8(cI-0.0001)local da=d8(d1-0.0001)local db=da+dg;local dc=(cK*da+d5*dg)/db;local dd=(cJ*da+d4*dg)/db;local ct,cv,cu,cw=ct,cv,cu,cw;local de=dc*10000*cv+ct;local df=dd*10000*cw+cu;if aF[10]or(de-h)*(Y-df)-(df-i)*(X-de)<0 then cx:drawTriangle(h,i,de,df,X,Y,aF[11],aF[12],s,d1,0.0001,d2)local d9=d8(d2-0.0001)local db=dg+d9;local dc=(cK*d9+d7*dg)/db;local dd=(cJ*d9+d6*dg)/db;local dh=dc*10000*cv+ct;local di=dd*10000*cw+cu;cx:drawTriangle(dh,di,de,df,X,Y,aF[11],aF[12],s,0.0001,0.0001,d2)end else local da=d8(d1-0.0001)local dg=d8(cI-0.0001)local d9=d8(d2-0.0001)local dj=da+dg;local dk=da+d9;local dc=(d5*dg+cK*da)/dj;local dd=(d4*dg+cJ*da)/dj;local ct,cv,cu,cw=ct,cv,cu,cw;local de=dc*10000*cv+ct;local df=dd*10000*cw+cu;local dl=(d5*d9+d7*da)/dk;local dm=(d4*d9+d6*da)/dk;local dh=dl*10000*cv+ct;local di=dm*10000*cw+cu;if aF[10]or(de-h)*(di-df)-(df-i)*(dh-de)<0 then cx:drawTriangle(h,i,de,df,dh,di,aF[11],aF[12],s,d1,0.0001,0.0001)end end end elseif cZ then local function d3(z,w,aN)local cF=z+cR;local cG=w+cS;local cH=aN+cT;local cI=cC*cF-cB*cH;cH=cB*cF+cC*cH;cF=cI;local cJ=cE*cG-cD*cF;cF=cD*cG+cE*cF;cG=cJ;if cz~=0 then local cK=cz*cH-cA*cG;cG=cA*cH+cz*cG;cH=cK end;local cL=cH/cF*cv+ct;local cM=cG/cF*cw+cu;return cL,cM,cF,cG,cH end;local h,i,d1,d4,d5=d3(aF[1],aF[2],aF[3])local j,k,cI,cJ,cK=d3(aF[4],aF[5],aF[6])local X,Y,d2,d6,d7=d3(aF[7],aF[8],aF[9])local d8=math.abs;if cI>0.00010000001 then if d2>0.00010000001 then local da=d8(d1-0.0001)local dg=d8(cI-0.0001)local db=da+dg;local dc=(d5*dg+cK*da)/db;local dd=(d4*dg+cJ*da)/db;local ct,cv,cu,cw=ct,cv,cu,cw;local de=dc*10000*cv+ct;local df=dd*10000*cw+cu;if aF[10]or(j-de)*(Y-k)-(k-df)*(X-j)<0 then cx:drawTriangle(de,df,j,k,X,Y,aF[11],aF[12],s,0.0001,cI,d2)local d9=d8(d2-0.0001)local db=da+d9;local dc=(d5*d9+d7*da)/db;local dd=(d4*d9+d6*da)/db;local dh=dc*10000*cv+ct;local di=dd*10000*cw+cu;cx:drawTriangle(de,df,dh,di,X,Y,aF[11],aF[12],s,0.0001,0.0001,d2)end else local da=d8(d1-0.0001)local dg=d8(cI-0.0001)local d9=d8(d2-0.0001)local dj=dg+da;local dk=dg+d9;local dc=(d5*dg+cK*da)/dj;local dd=(d4*dg+cJ*da)/dj;local ct,cv,cu,cw=ct,cv,cu,cw;local de=dc*10000*cv+ct;local df=dd*10000*cw+cu;local dl=(cK*d9+d7*dg)/dk;local dm=(cJ*d9+d6*dg)/dk;local dh=dl*10000*cv+ct;local di=dm*10000*cw+cu;if aF[10]or(j-de)*(di-k)-(k-df)*(dh-j)<0 then cx:drawTriangle(de,df,j,k,dh,di,aF[11],aF[12],s,0.0001,cI,0.0001)end end else if d2>0.00010000001 then local da=d8(d1-0.0001)local dg=d8(cI-0.0001)local d9=d8(d2-0.0001)local dj=d9+da;local dk=d9+dg;local dc=(d5*d9+d7*da)/dj;local dd=(d4*d9+d6*da)/dj;local ct,cv,cu,cw=ct,cv,cu,cw;local de=dc*10000*cv+ct;local df=dd*10000*cw+cu;local dl=(cK*d9+d7*dg)/dk;local dm=(cJ*d9+d6*dg)/dk;local dh=dl*10000*cv+ct;local di=dm*10000*cw+cu;if aF[10]or(dh-de)*(Y-di)-(di-df)*(X-dh)<0 then cx:drawTriangle(de,df,dh,di,X,Y,aF[11],aF[12],s,0.0001,0.0001,d2)end end end end end end;function cs:drawObjects(dn)self.buffer:clearDepth()local ax=self.camera;local cN={aK(ax[4]or 0),aL(ax[4]or 0),aK(-ax[5]),aL(-ax[5]),aK(ax[6]),aL(ax[6])}local dn=dn;for aE=1,#dn do self:drawObject(dn[aE],ax,cN)end end;function cs:drawBuffer()local cx=self.buffer;cx:drawBuffer()cx:fastClear()end;function cs:setCamera(dp,dq,dr,aU,aV,aW)local aJ=math.rad;if type(dp)=="table"then local ax=dp;self.camera={ax.x or self.camera[1]or 0,ax.y or self.camera[2]or 0,ax.z or self.camera[3]or 0,ax.rotX and aJ(ax.rotX+90)or self.camera[4]or 0,ax.rotY and aJ(ax.rotY)or self.camera[5]or 0,ax.rotZ and aJ(ax.rotZ)or self.camera[6]or 0,self.camera[7],self.camera[8]}else self.camera={dp or self.camera[1]or 0,dq or self.camera[2]or 0,dr or self.camera[3]or 0,aU and aJ(aU+90)or self.camera[4]or 0,aV and aJ(aV)or self.camera[5]or 0,aW and aJ(aW)or self.camera[6]or 0,self.camera[7],self.camera[8]}end;if self.camera[4]==math.pi*0.5 then self.camera[4]=nil end end;function cs:setFoV(cU)self.FoV=cU or 90;self.t=cp(aJ(self.FoV/2))*2*0.0001;cy()self.camera[7]=aJ(self.FoV)self.camera[8]=2*math.atan(math.tan(aJ(self.FoV)*0.5)/(self.width/self.height/1.5))end;function cs:setWireFrame(ar)self.buffer:useTriangleEdges(ar)end;function cs:getObjectIndexTrace(dn,z,w)local function ds(h,i,j,k,X,Y)return(h-X)*(k-Y)-(j-X)*(i-Y)end;local function dt(du,dv,h,i,j,k,X,Y,dw,dx,dy,dz)local dA=ds(du,dv,h,i,j,k)<0;local dB=ds(du,dv,j,k,X,Y)<0;local dC=ds(du,dv,X,Y,h,i)<0;return dA==dB and dB==dC end;local w=w-1;local dD=self.x1;local dE=self.y1;local dF=self.x2;local dG=self.y2;z=z*2;w=w*3+1;local ax=self.camera;local cN={aK(ax[4]or 0),aL(ax[4]or 0),aK(-ax[5]),aL(-ax[5]),aK(ax[6]),aL(ax[6])}local cz=cN[1]local cA=cN[2]local cB=cN[3]local cC=cN[4]local cD=cN[5]local cE=cN[6]local dH={}for aE=1,#dn do local ca=dn[aE]local aT=ca[7]local aU=ca[4]local aV=ca[5]local aW=ca[6]if aU and aU~=0 or aV and aV~=0 or aW and aW~=0 then aT=aS(aT,aU,aV,aW)end;local cO=ca[1]local cP=ca[2]local cQ=ca[3]as(aT,cO,cP,cQ,ax)local ct=ct;local cu=cu;local cv=cv;local cw=cw;local cz=cz;local cA=cA;local cB=cB;local cC=cC;local cD=cD;local cE=cE;local cR=cO-ax[1]local cS=cP-ax[2]local cT=cQ-ax[3]local function c_(z,w,aN)local cF=z+cR;local cG=w+cS;local cH=aN+cT;local cI=cC*cF-cB*cH;cH=cB*cF+cC*cH;cF=cI;local cJ=cE*cG-cD*cF;cF=cD*cG+cE*cF;cG=cJ;if cz~=0 then local cK=cz*cH-cA*cG;cG=cA*cH+cz*cG;cH=cK end;local cL=cH/cF*cv+ct;local cM=cG/cF*cw+cu;return cL,cM,cF>0 end;for c6=1,#aT do local aF=aT[c6]local h,i,dI=c_(aF[1],aF[2],aF[3])if dI then local j,k,dJ=c_(aF[4],aF[5],aF[6])if dJ then local X,Y,dK=c_(aF[7],aF[8],aF[9])if dK then if aF[10]or(j-h)*(Y-k)-(k-i)*(X-j)<0 then local aG=cR+(aF[1]+aF[4]+aF[7])/3;local aH=cS+(aF[2]+aF[5]+aF[8])/3;local aI=cT+(aF[3]+aF[6]+aF[9])/3;local s=aG*aG+aH*aH+aI*aI;if dt(z,w,h,i,j,k,X,Y,(dF-1)*2+1,(dE-1)*3+1,dF*2,(dG+1)*3)then dH[#dH+1]={objectIndex=aE,polygonIndex=c6,depth=s}end end end end end end end;if#dH<=0 then return elseif#dH==1 then return dH[1].objectIndex,dH[1].polygonIndex,dH[1].depth end;local dL=-1;local dM=math.huge;for aE=1,#dH do local dN=dH[aE]if dN.depth=0 then bb.c=dY.top or bb.c else bb.c=dY.bottom or bb.c end end end end;for bP,bQ in pairs(b4)do aT[bP]=bQ end;return aT end;function dW:icosphere(dY)dY.res=dY.res or 1;local e3=(1+cf(5))/2;local e4={{e3,1,0},{e3,-1,0},{-e3,-1,0},{-e3,1,0},{1,0,e3},{-1,0,e3},{-1,0,-e3},{1,0,-e3},{0,e3,1},{0,e3,-1},{0,-e3,-1},{0,-e3,1}}local function e5(bo,bp,bq)return dX(e4[bo][1],e4[bo][2],e4[bo][3],e4[bp][1],e4[bp][2],e4[bp][3],e4[bq][1],e4[bq][2],e4[bq][3],dY.colors and 1 or dY.color)end;local aT={e5(11,2,12),e5(11,8,2),e5(11,7,8),e5(11,3,7),e5(11,12,3),e5(4,7,3),e5(4,10,7),e5(4,9,10),e5(4,6,9),e5(4,3,6),e5(5,6,12),e5(5,9,6),e5(5,1,9),e5(5,2,1),e5(5,12,2),e5(3,12,6),e5(1,8,10),e5(1,10,9),e5(1,2,8),e5(10,8,7)}local function e6()local bN={}for aE=1,#aT do local bb=aT[aE]local e7={x=(bb.x1+bb.x2)/2,y=(bb.y1+bb.y2)/2,z=(bb.z1+bb.z2)/2}local e8={x=(bb.x1+bb.x3)/2,y=(bb.y1+bb.y3)/2,z=(bb.z1+bb.z3)/2}local e9={x=(bb.x2+bb.x3)/2,y=(bb.y2+bb.y3)/2,z=(bb.z2+bb.z3)/2}local ea=bb.c;if dY.colorsFractal then ea=ea%#dY.colors+1 end;bN[#bN+1]=dX(e7.x,e7.y,e7.z,e9.x,e9.y,e9.z,e8.x,e8.y,e8.z,bb.c)bN[#bN+1]=dX(bb.x1,bb.y1,bb.z1,e7.x,e7.y,e7.z,e8.x,e8.y,e8.z,ea)bN[#bN+1]=dX(e7.x,e7.y,e7.z,bb.x2,bb.y2,bb.z2,e9.x,e9.y,e9.z,ea)bN[#bN+1]=dX(e8.x,e8.y,e8.z,e9.x,e9.y,e9.z,bb.x3,bb.y3,bb.z3,ea)end;aT=bN end;for aE=1,dY.res-1 do e6()end;local function eb(z,w,aN)local ec=math.sqrt(z*z+w*w+aN*aN)local ed=0.5/ec;return z*ed,w*ed,aN*ed end;for aE=1,#aT do local bb=aT[aE]bb.x1,bb.y1,bb.z1=eb(bb.x1,bb.y1,bb.z1)bb.x2,bb.y2,bb.z2=eb(bb.x2,bb.y2,bb.z2)bb.x3,bb.y3,bb.z3=eb(bb.x3,bb.y3,bb.z3)if not dY.colorsFractal then local aH=(bb.y1+bb.y2+bb.y3)/3;if dY.colors then local e2=math.floor((-aH+0.5)*#dY.colors+1)bb.c=dY.colors[e2]or bb.c else if aH>=0 then bb.c=dY.top or bb.c else bb.c=dY.bottom or bb.c end end else bb.c=dY.colors[bb.c]end end;for bP,bQ in pairs(b4)do aT[bP]=bQ end;return aT end;function dW:plane(dY)dY.color=dY.color or colors.lime;dY.size=dY.size or 1;dY.y=dY.y or 0;local ee={dX(-1*dY.size,dY.y,1*dY.size,1*dY.size,dY.y,-1*dY.size,-1*dY.size,dY.y,-1*dY.size,dY.color),dX(-1*dY.size,dY.y,1*dY.size,1*dY.size,dY.y,1*dY.size,1*dY.size,dY.y,-1*dY.size,dY.color)}for bP,bQ in pairs(b4)do ee[bP]=bQ end;return ee end;function dW:mountains(dY)dY.res=dY.res or 20;dY.randomOffset=dY.randomOffset or 0;dY.height=dY.height or 1;dY.randomHeight=dY.randomHeight or 0;dY.y=dY.y or 0;dY.scale=dY.scale or 100;dY.color=dY.color or colors.green;dY.snowColor=dY.snowColor or colors.white;local ef=3/dY.res*dY.height/(dY.randomHeight+1)local eg=3/dY.res*dY.height*(dY.randomHeight+1)local aT={}for aE=0,dY.res do local eh=math.random(-dY.randomOffset*100,dY.randomOffset*100)/100;local ei=aE+eh;local h=aL((ei-1)/dY.res*co*2)*dY.scale;local aa=aK((ei-1)/dY.res*co*2)*dY.scale;local j=aL((ei-0.5)/dY.res*co*2)*dY.scale;local ab=aK((ei-0.5)/dY.res*co*2)*dY.scale;local X=aL(ei/dY.res*co*2)*dY.scale;local ac=aK(ei/dY.res*co*2)*dY.scale;local ej=math.random(ef*100,eg*100)/100*dY.scale;local aF={x1=h,y1=dY.y,z1=aa,x2=X,y2=dY.y,z2=ac,x3=j,y3=dY.y+ej,z3=ab,c=dY.color,forceRender=true}aT[#aT+1]=aF;if dY.snow then local ek=0.93;local el=dY.snowHeight or 0.5;local em=1-el*eg/(ej/dY.scale)em=d(0,c(1,em))if em>0.2 then local en={x1=(h*em+j*(1-em))*ek,y1=dY.y+ej*(1-em),z1=(aa*em+ab*(1-em))*ek,x2=(X*em+j*(1-em))*ek,y2=dY.y+ej*(1-em),z2=(ac*em+ab*(1-em))*ek,x3=j*ek,y3=dY.y+ej,z3=ab*ek,c=dY.snowColor,forceRender=true}aT[#aT+1]=en end end end;for bP,bQ in pairs(b4)do aT[bP]=bQ end;return aT end;return{newFrame=cq,loadModel=bR,newBuffer=g,models=dW,transforms=b4} -------------------------------------------------------------------------------- /Pine3D.lua: -------------------------------------------------------------------------------- 1 | -- Made by Xella#8655 2 | 3 | local libFolder = (...):match("(.-)[^%.]+$") 4 | local betterblittle = require(libFolder .. "betterblittle") 5 | 6 | local min = math.min 7 | local max = math.max 8 | local floor = math.floor 9 | local ceil = math.ceil 10 | 11 | ---Creates a new Buffer for rendering triangles 12 | ---@param x integer the new x position of the window 13 | ---@param y integer the new y position of the window 14 | ---@param w integer the new width of the window 15 | ---@param h integer the new height of the window 16 | ---@return Buffer 17 | local function newBuffer(x, y, w, h) 18 | ---@class Buffer 19 | local buffer = { 20 | width = w * 2, 21 | height = h * 3, 22 | colorValues = {}, 23 | depthValues = {}, 24 | blitWin = window.create(term.current(), x, y, w, h, false), 25 | backgroundColor = colors.lightBlue 26 | } 27 | 28 | ---Reposition the Buffer 29 | ---@param x integer the new x position of the window 30 | ---@param y integer the new y position of the window 31 | ---@param w integer the new width of the window 32 | ---@param h integer the new height of the window 33 | function buffer:setSize(x, y, w, h) 34 | self.width = w * 2 35 | self.height = h * 3 36 | self.blitWin.reposition(x, y, w, h) 37 | self:clear() 38 | end 39 | 40 | ---Fully clear the Buffer 41 | function buffer:clear() 42 | self.colorValues = {} 43 | self.depthValues = {} 44 | local colors = self.colorValues 45 | local depths = self.depthValues 46 | 47 | local bigNeg = math.huge 48 | local width = self.width 49 | local color = self.backgroundColor 50 | 51 | for y = 1, self.height do 52 | colors[y] = {} 53 | depths[y] = {} 54 | local colorsY = colors[y] 55 | local depthsY = depths[y] 56 | for x = 1, width do 57 | colorsY[x] = color 58 | depthsY[x] = bigNeg 59 | end 60 | end 61 | end 62 | 63 | function buffer:clearDepth() 64 | local depths = self.depthValues 65 | 66 | local bigNeg = -math.huge 67 | local width = self.width 68 | 69 | for y = 1, self.height do 70 | local depthsY = depths[y] 71 | for x = 1, width do 72 | depthsY[x] = bigNeg 73 | end 74 | end 75 | end 76 | 77 | ---Clear the Buffer quickly 78 | function buffer:fastClear() 79 | local bgColor = self.backgroundColor 80 | local colors = self.colorValues 81 | 82 | local width = self.width 83 | for y = 1, self.height do 84 | local colorsY = colors[y] 85 | for x = 1, width do 86 | colorsY[x] = bgColor 87 | end 88 | end 89 | end 90 | 91 | ---Set the color of an individual pixel 92 | ---@param x number teletext pixel x coordinate 93 | ---@param y number teletext pixel y coordinate 94 | ---@param c integer color for the pixel 95 | function buffer:setPixel(x, y, c) 96 | x = floor(x + 0.5) 97 | y = floor(y + 0.5) 98 | 99 | if x >= 1 and x <= self.width then 100 | if y >= 1 and y <= self.height then 101 | self.colorValues[y][x] = c 102 | end 103 | end 104 | end 105 | 106 | ---@alias PaintUtilsImage table 107 | ---Render an image to the Buffer at a given offset 108 | ---@param x integer teletext pixel x coordinate 109 | ---@param y integer teletext pixel y coordinate 110 | ---@param image PaintUtilsImage can be loaded with paintutils.loadImage (from .nfp) 111 | function buffer:image(x, y, image) 112 | local width = self.width 113 | local colors = self.colorValues 114 | 115 | for yImage, row in pairs(image) do 116 | local drawY = yImage + y 117 | local colorsY = colors[drawY] 118 | if colorsY then 119 | for xImage, value in pairs(row) do 120 | if value and value > 0 then 121 | local drawX = xImage + x 122 | if drawX >= 1 and drawX <= width then 123 | colorsY[drawX] = value 124 | end 125 | end 126 | end 127 | end 128 | end 129 | end 130 | 131 | function buffer:drawLineNoInterp(x1, y1, x2, y2, c, depthInverted) 132 | local colors = self.colorValues 133 | local depths = self.depthValues 134 | 135 | local frameWidth = self.width 136 | local frameHeight = self.height 137 | 138 | local dx = x2 - x1 139 | local dy = y2 - y1 140 | 141 | if dx ~= 0 then 142 | for x = max(ceil(min(x1, x2)), 1), min(floor(max(x1, x2)), frameWidth) do 143 | local xRatio = (x - x1) / dx 144 | local y = floor(y1 + xRatio * dy + 0.5) 145 | if y > 0 and y <= frameHeight and depthInverted >= depths[y][x] then 146 | colors[y][x] = c 147 | depths[y][x] = depthInverted 148 | end 149 | end 150 | end 151 | if dy ~= 0 then 152 | for y = max(ceil(min(y1, y2)), 1), min(floor(max(y1, y2)), frameHeight) do 153 | local yRatio = (y - y1) / dy 154 | local x = floor(x1 + yRatio * dx + 0.5) 155 | if x > 0 and x <= frameWidth and depthInverted >= depths[y][x] then 156 | colors[y][x] = c 157 | depths[y][x] = depthInverted 158 | end 159 | end 160 | end 161 | end 162 | 163 | function buffer:drawLineInterp(x1, y1, x2, y2, c, z1Inv, z2Inv) 164 | local colors = self.colorValues 165 | local depths = self.depthValues 166 | 167 | local frameWidth = self.width 168 | local frameHeight = self.height 169 | 170 | x1 = x1 + 0.5 171 | x2 = x2 + 0.5 172 | y1 = y1 - 0.5 173 | y2 = y2 - 0.5 174 | 175 | local dx = x2 - x1 176 | local dy = y2 - y1 177 | local slopeZInv = z2Inv - z1Inv 178 | 179 | if dx ~= 0 then 180 | for x = max(ceil(min(x1, x2)), 1), min(floor(max(x1, x2)), frameWidth) do 181 | local xRatio = (x - x1) / dx 182 | 183 | local y = floor(y1 + xRatio * dy + 0.5) 184 | local zInv = z1Inv + xRatio * slopeZInv 185 | if y > 0 and y <= frameHeight and zInv >= depths[y][x] then 186 | colors[y][x] = c 187 | depths[y][x] = zInv 188 | end 189 | end 190 | end 191 | if dy ~= 0 then 192 | for y = max(ceil(min(y1, y2)), 1), min(floor(max(y1, y2)), frameHeight) do 193 | local yRatio = (y - y1) / dy 194 | 195 | local x = floor(x1 + yRatio * dx + 0.5) 196 | local zInv = z1Inv + yRatio * slopeZInv 197 | if x > 0 and x <= frameWidth and zInv >= depths[y][x] then 198 | colors[y][x] = c 199 | depths[y][x] = zInv 200 | end 201 | end 202 | end 203 | end 204 | 205 | local defaultOutlineColor = colors.black 206 | 207 | function buffer:drawTriangleNoInterp(x1, y1, x2, y2, x3, y3, c, outlineColor, depth) 208 | if x1 < 0 and x2 < 0 and x3 < 0 or y1 < 1 and y2 < 1 and y3 < 1 then return end 209 | 210 | local frameWidth = self.width 211 | if x1 > frameWidth and x2 > frameWidth and x3 > frameWidth then return end 212 | local frameHeight = self.height 213 | if y1 > frameHeight and y2 > frameHeight and y3 > frameHeight then return end 214 | 215 | if y1 > y2 then 216 | y1, y2 = y2, y1 217 | x1, x2 = x2, x1 218 | end 219 | if y2 > y3 then 220 | y3, y2 = y2, y3 221 | x3, x2 = x2, x3 222 | end 223 | if y1 > y2 then 224 | y1, y2 = y2, y1 225 | x1, x2 = x2, x1 226 | end 227 | 228 | local floor, ceil = floor, ceil 229 | local min, max = min, max 230 | 231 | local minY = min(max(1, ceil(y1)), frameHeight) 232 | local midY = min(max(0, floor(y2)), frameHeight) 233 | local maxY = min(max(1, floor(y3)), frameHeight) 234 | 235 | local colors = self.colorValues 236 | local depths = self.depthValues 237 | local depthInverted = 1 / depth 238 | 239 | if y1 ~= y2 and y1 ~= y3 then 240 | local slopeX_YA = (x2 - x1) / (y2 - y1) 241 | local slopeX_YB = (x3 - x1) / (y3 - y1) 242 | 243 | for y = minY, midY do 244 | local dy = y - y1 245 | local xA = x1 + dy * slopeX_YA 246 | local xB = x1 + dy * slopeX_YB 247 | if xB < xA then xA, xB = xB, xA end 248 | 249 | local xABounded = floor(xA + 0.5) 250 | local xBBounded = floor(xB + 0.5) 251 | if xABounded < 1 then xABounded = 1 end 252 | if xBBounded > frameWidth then xBBounded = frameWidth end 253 | 254 | local colorsY = colors[y] 255 | local depthsY = depths[y] 256 | 257 | for x = xABounded, xBBounded do 258 | if depthInverted > depthsY[x] then 259 | depthsY[x] = depthInverted 260 | colorsY[x] = c 261 | end 262 | end 263 | end 264 | end 265 | 266 | if y3 ~= y2 and y1 ~= y3 then 267 | local slopeX_YA = (x2 - x3) / (y2 - y3) 268 | local slopeX_YB = (x1 - x3) / (y1 - y3) 269 | 270 | for y = midY + 1, maxY do 271 | local dy = y - y3 272 | local xA = x3 + dy * slopeX_YA 273 | local xB = x3 + dy * slopeX_YB 274 | if xB < xA then xA, xB = xB, xA end 275 | 276 | local xABounded = floor(xA + 0.5) 277 | local xBBounded = floor(xB + 0.5) 278 | if xABounded < 1 then xABounded = 1 end 279 | if xBBounded > frameWidth then xBBounded = frameWidth end 280 | 281 | local colorsY = colors[y] 282 | local depthsY = depths[y] 283 | 284 | for x = xABounded, xBBounded do 285 | if depthInverted > depthsY[x] then 286 | depthsY[x] = depthInverted 287 | colorsY[x] = c 288 | end 289 | end 290 | end 291 | end 292 | 293 | local outlineColor = outlineColor 294 | if outlineColor or self.triangleEdges then 295 | local loadLine = self.drawLineNoInterp 296 | local c = outlineColor or defaultOutlineColor 297 | loadLine(self, x1, y1, x2, y2, c, depthInverted) 298 | loadLine(self, x2, y2, x3, y3, c, depthInverted) 299 | loadLine(self, x3, y3, x1, y1, c, depthInverted) 300 | end 301 | end 302 | 303 | function buffer:drawTriangleInterp(x1, y1, x2, y2, x3, y3, c, outlineColor, d, z1, z2, z3) 304 | if x1 < 0 and x2 < 0 and x3 < 0 or y1 < 1 and y2 < 1 and y3 < 1 then return end 305 | 306 | local frameWidth = self.width 307 | if x1 > frameWidth and x2 > frameWidth and x3 > frameWidth then return end 308 | local frameHeight = self.height 309 | if y1 > frameHeight and y2 > frameHeight and y3 > frameHeight then return end 310 | 311 | if y1 > y2 then 312 | y1, y2 = y2, y1 313 | x1, x2 = x2, x1 314 | z1, z2 = z2, z1 315 | end 316 | if y2 > y3 then 317 | y3, y2 = y2, y3 318 | x3, x2 = x2, x3 319 | z3, z2 = z2, z3 320 | end 321 | if y1 > y2 then 322 | y1, y2 = y2, y1 323 | x1, x2 = x2, x1 324 | z1, z2 = z2, z1 325 | end 326 | 327 | local floor, ceil = floor, ceil 328 | local min, max = min, max 329 | 330 | local minY = min(max(1, ceil(y1)), frameHeight) 331 | local midY = min(max(0, floor(y2)), frameHeight) 332 | local maxY = min(max(1, floor(y3)), frameHeight) 333 | 334 | local colors = self.colorValues 335 | local depths = self.depthValues 336 | local z3Inv = 1 / z3 337 | local z2Inv = 1 / z2 338 | local z1Inv = 1 / z1 339 | 340 | if y1 ~= y2 and y1 ~= y3 then 341 | local y2_min_y1 = y2 - y1 342 | local y3_min_y1 = y3 - y1 343 | 344 | local slopeX_YA = (x2 - x1) / y2_min_y1 345 | local slopeX_YB = (x3 - x1) / y3_min_y1 346 | local slopeZ_YAInv = z2Inv - z1Inv 347 | local slopeZ_YBInv = z3Inv - z1Inv 348 | 349 | for y = minY, midY do 350 | local dy = y - y1 351 | local xA = x1 + dy * slopeX_YA 352 | local xB = x1 + dy * slopeX_YB 353 | local yRatioA = dy / y2_min_y1 354 | local yRatioB = dy / y3_min_y1 355 | local zAInv = z1Inv + yRatioA * slopeZ_YAInv 356 | local zBInv = z1Inv + yRatioB * slopeZ_YBInv 357 | if xB < xA then 358 | xA, xB = xB, xA 359 | zAInv, zBInv = zBInv, zAInv 360 | end 361 | 362 | local xABounded = floor(xA + 1.5) 363 | local xBBounded = floor(xB + 0.5) 364 | if xABounded < 1 then xABounded = 1 end 365 | if xBBounded > frameWidth then xBBounded = frameWidth end 366 | 367 | xA = floor(xA + 0.5) 368 | xB = floor(xB + 1.5) 369 | local xLength = xB - xA 370 | 371 | local colorsY = colors[y] 372 | local depthsY = depths[y] 373 | local slopeZ_XInv = zBInv - zAInv 374 | 375 | for x = xABounded, xBBounded do 376 | local xRatio = (x - xA) / xLength 377 | local zInv = zAInv + xRatio * slopeZ_XInv 378 | if zInv > depthsY[x] then 379 | depthsY[x] = zInv 380 | colorsY[x] = c 381 | end 382 | end 383 | end 384 | end 385 | 386 | if y3 ~= y2 and y1 ~= y3 then 387 | local y2_min_y3 = y2 - y3 388 | local y1_min_y3 = y1 - y3 389 | 390 | local slopeX_YA = (x2 - x3) / y2_min_y3 391 | local slopeX_YB = (x1 - x3) / y1_min_y3 392 | local slopeZ_YAInv = z2Inv - z3Inv 393 | local slopeZ_YBInv = z1Inv - z3Inv 394 | 395 | for y = midY + 1, maxY do 396 | local dy = y - y3 397 | local xA = x3 + dy * slopeX_YA 398 | local xB = x3 + dy * slopeX_YB 399 | local yRatioA = dy / y2_min_y3 400 | local yRatioB = dy / y1_min_y3 401 | local zAInv = z3Inv + yRatioA * slopeZ_YAInv 402 | local zBInv = z3Inv + yRatioB * slopeZ_YBInv 403 | 404 | if xB < xA then 405 | xA, xB = xB, xA 406 | zAInv, zBInv = zBInv, zAInv 407 | end 408 | 409 | local xABounded = floor(xA + 1.5) 410 | local xBBounded = floor(xB + 0.5) 411 | if xABounded < 1 then xABounded = 1 end 412 | if xBBounded > frameWidth then xBBounded = frameWidth end 413 | 414 | xA = floor(xA + 0.5) 415 | xB = floor(xB + 1.5) 416 | local xLength = xB - xA 417 | 418 | local colorsY = colors[y] 419 | local depthsY = depths[y] 420 | local slopeZ_XInv = zBInv - zAInv 421 | 422 | for x = xABounded, xBBounded do 423 | local xRatio = (x - xA) / xLength 424 | local zInv = zAInv + xRatio * slopeZ_XInv 425 | if zInv > depthsY[x] then 426 | depthsY[x] = zInv 427 | colorsY[x] = c 428 | end 429 | end 430 | end 431 | end 432 | 433 | local outlineColor = outlineColor 434 | if outlineColor or self.triangleEdges then 435 | local loadLine = self.drawLineInterp 436 | local c = outlineColor or defaultOutlineColor 437 | loadLine(self, x1, y1, x2, y2, c, z1Inv, z2Inv) 438 | loadLine(self, x2, y2, x3, y3, c, z2Inv, z3Inv) 439 | loadLine(self, x3, y3, x1, y1, c, z3Inv, z1Inv) 440 | end 441 | end 442 | 443 | function buffer:drawBuffer() 444 | local blitWin = self.blitWin 445 | betterblittle.drawBuffer(self.colorValues, blitWin) 446 | blitWin.setVisible(true) 447 | blitWin.setVisible(false) 448 | end 449 | 450 | function buffer:depthInterpolation(enabled) 451 | self.drawTriangle = enabled and self.drawTriangleInterp or self.drawTriangleNoInterp 452 | self:clear() 453 | end 454 | 455 | function buffer:useTriangleEdges(enabled) 456 | self.triangleEdges = enabled 457 | end 458 | 459 | buffer:depthInterpolation(true) 460 | 461 | return buffer 462 | end 463 | 464 | ---Computes distance to comera for each polygon 465 | ---@param polygons Polygon[] 466 | ---@param objectX number 467 | ---@param objectY number 468 | ---@param objectZ number 469 | ---@param camera CollapsedCamera 470 | local function computePolyCamDistance(polygons, objectX, objectY, objectZ, camera) 471 | local camX = camera[1] 472 | local camY = camera[2] 473 | local camZ = camera[3] 474 | local rx = objectX and (objectX - camX) or 0 475 | local ry = objectY and (objectY - camY) or 0 476 | local rz = objectZ and (objectZ - camZ) or 0 477 | 478 | for i = 1, #polygons do 479 | local polygon = polygons[i] 480 | 481 | local avgX = rx + (polygon[1] + polygon[4] + polygon[7]) / 3 482 | local avgY = ry + (polygon[2] + polygon[5] + polygon[8]) / 3 483 | local avgZ = rz + (polygon[3] + polygon[6] + polygon[9]) / 3 484 | 485 | polygon[16] = avgX * avgX + avgY * avgY + avgZ * avgZ -- relative distance 486 | end 487 | end 488 | 489 | local rad = math.rad 490 | local sin = math.sin 491 | local cos = math.cos 492 | local function rotatePolygonX(x, y, z, rotS, rotC) 493 | local z2 = rotC * z - rotS * y 494 | local y = rotS * z + rotC * y 495 | local z = z2 496 | return x, y, z 497 | end 498 | local function rotatePolygonY(x, y, z, rotS, rotC) 499 | local z2 = rotC * z - rotS * x 500 | local x = rotS * z + rotC * x 501 | local z = z2 502 | return x, y, z 503 | end 504 | local function rotatePolygonZ(x, y, z, rotS, rotC) 505 | local y2 = rotC * y - rotS * x 506 | local x = rotS * y + rotC * x 507 | local y = y2 508 | return x, y 509 | end 510 | 511 | ---Rotates a given CollapsedModel around three axes 512 | ---@param model CollapsedModel 513 | ---@param rotX number? 514 | ---@param rotY number? 515 | ---@param rotZ number? 516 | ---@return CollapsedModel 517 | local function rotateCollapsedModel(model, rotX, rotY, rotZ) 518 | local rotXS, rotXC = 0, 1 519 | local rotYS, rotYC = 0, 1 520 | local rotZS, rotZC = 0, 1 521 | 522 | if rotX == 0 then rotX = nil end 523 | if rotX then rotXS, rotXC = sin(rotX), cos(rotX) end 524 | if rotY == 0 then rotY = nil end 525 | if rotY then rotYS, rotYC = sin(rotY), cos(rotY) end 526 | if rotZ == 0 then rotZ = nil end 527 | if rotZ then rotZS, rotZC = sin(rotZ), cos(rotZ) end 528 | 529 | local rotatedModel = {} 530 | 531 | for _, polygon in pairs(model) do 532 | local x1, y1, z1 = polygon[1], polygon[2], polygon[3] 533 | local x2, y2, z2 = polygon[4], polygon[5], polygon[6] 534 | local x3, y3, z3 = polygon[7], polygon[8], polygon[9] 535 | 536 | if rotY then 537 | x1, y1, z1 = rotatePolygonY(x1, y1, z1, rotYS, rotYC) 538 | x2, y2, z2 = rotatePolygonY(x2, y2, z2, rotYS, rotYC) 539 | x3, y3, z3 = rotatePolygonY(x3, y3, z3, rotYS, rotYC) 540 | end 541 | 542 | if rotZ then 543 | x1, y1 = rotatePolygonZ(x1, y1, z1, rotZS, rotZC) 544 | x2, y2 = rotatePolygonZ(x2, y2, z2, rotZS, rotZC) 545 | x3, y3 = rotatePolygonZ(x3, y3, z3, rotZS, rotZC) 546 | end 547 | 548 | if rotX then 549 | x1, y1, z1 = rotatePolygonX(x1, y1, z1, rotXS, rotXC) 550 | x2, y2, z2 = rotatePolygonX(x2, y2, z2, rotXS, rotXC) 551 | x3, y3, z3 = rotatePolygonX(x3, y3, z3, rotXS, rotXC) 552 | end 553 | 554 | rotatedModel[#rotatedModel + 1] = {x1, y1, z1, x2, y2, z2, x3, y3, z3} 555 | rotatedModel[#rotatedModel][10] = polygon[10] 556 | rotatedModel[#rotatedModel][11] = polygon[11] 557 | rotatedModel[#rotatedModel][12] = polygon[12] 558 | end 559 | 560 | return rotatedModel 561 | end 562 | 563 | local transforms = {} 564 | ---Model Inverts the direction of all triangles 565 | ---@param model Model 566 | ---@return Model 567 | function transforms.invertTriangles(model) 568 | if not model or type(model) ~= "table" then 569 | error("transforms.invertTriangles expected arg#1 to be a table (model)") 570 | end 571 | 572 | for i = 1, #model do 573 | local triangle = model[i] 574 | model[i] = { 575 | x1 = triangle.x1, 576 | y1 = triangle.y1, 577 | z1 = triangle.z1, 578 | x2 = triangle.x3, 579 | y2 = triangle.y3, 580 | z2 = triangle.z3, 581 | x3 = triangle.x2, 582 | y3 = triangle.y2, 583 | z3 = triangle.z2, 584 | c = triangle.c, 585 | forceRender = triangle.forceRender, 586 | outlineColor = triangle.outlineColor, 587 | } 588 | end 589 | return model 590 | end 591 | 592 | ---Change the outline colors of polygons in a Model 593 | ---@param model Model 594 | ---@param col number|table if number, will set the outline color for each Polygon, if table, uses it as a mapping from Polygon color to new outline color 595 | ---@return Model 596 | function transforms.setOutline(model, col) 597 | if not model or type(model) ~= "table" then 598 | error("transforms.invertTriangles expected arg#1 to be a table (model)") 599 | end 600 | 601 | for i = 1, #model do 602 | local triangle = model[i] 603 | if type(col) == "table" then -- colormap 604 | triangle.outlineColor = col[triangle.c] or triangle.outlineColor 605 | else 606 | triangle.outlineColor = col 607 | end 608 | end 609 | return model 610 | end 611 | 612 | ---Change the colors of polygons in a Model 613 | ---@param model Model 614 | ---@param col number|table if number, will set the color for each Polygon, if table, uses it as a mapping from Polygon color to new the new color 615 | ---@return Model 616 | function transforms.mapColor(model, col) 617 | if not model or type(model) ~= "table" then 618 | error("transforms.mapColor expected arg#1 to be a table (model)") 619 | end 620 | 621 | for i = 1, #model do 622 | local triangle = model[i] 623 | if type(col) == "table" then -- colormap 624 | triangle.c = col[triangle.c] or triangle.c 625 | else 626 | triangle.c = col 627 | end 628 | end 629 | return model 630 | end 631 | 632 | ---Center the Model such that the origin is in the middle of the bounding box 633 | ---@param model Model 634 | function transforms.center(model) 635 | local minX, maxX = math.huge, -math.huge 636 | local minY, maxY = math.huge, -math.huge 637 | local minZ, maxZ = math.huge, -math.huge 638 | 639 | for i = 1, #model do 640 | local poly = model[i] 641 | minX, maxX = min(minX, poly.x1), max(maxX, poly.x1) 642 | minX, maxX = min(minX, poly.x2), max(maxX, poly.x2) 643 | minX, maxX = min(minX, poly.x3), max(maxX, poly.x3) 644 | minY, maxY = min(minY, poly.y1), max(maxY, poly.y1) 645 | minY, maxY = min(minY, poly.y2), max(maxY, poly.y2) 646 | minY, maxY = min(minY, poly.y3), max(maxY, poly.y3) 647 | minZ, maxZ = min(minZ, poly.z1), max(maxZ, poly.z1) 648 | minZ, maxZ = min(minZ, poly.z2), max(maxZ, poly.z2) 649 | minZ, maxZ = min(minZ, poly.z3), max(maxZ, poly.z3) 650 | end 651 | 652 | local offsetX = -(maxX + minX) * 0.5 653 | local offsetY = -(maxY + minY) * 0.5 654 | local offsetZ = -(maxZ + minZ) * 0.5 655 | 656 | for i = 1, #model do 657 | local poly = model[i] 658 | poly.x1 = poly.x1 + offsetX 659 | poly.x2 = poly.x2 + offsetX 660 | poly.x3 = poly.x3 + offsetX 661 | poly.y1 = poly.y1 + offsetY 662 | poly.y2 = poly.y2 + offsetY 663 | poly.y3 = poly.y3 + offsetY 664 | poly.z1 = poly.z1 + offsetZ 665 | poly.z2 = poly.z2 + offsetZ 666 | poly.z3 = poly.z3 + offsetZ 667 | end 668 | 669 | return model, offsetX, offsetY, offsetZ 670 | end 671 | 672 | ---Rescales the model such that the largest value of any coordinate is equal to 1 673 | ---@param model Model 674 | function transforms.normalizeScale(model) 675 | local maxVal = -math.huge 676 | 677 | for i = 1, #model do 678 | local poly = model[i] 679 | maxVal = max(maxVal, poly.x1) 680 | maxVal = max(maxVal, poly.x2) 681 | maxVal = max(maxVal, poly.x3) 682 | maxVal = max(maxVal, poly.y1) 683 | maxVal = max(maxVal, poly.y2) 684 | maxVal = max(maxVal, poly.y3) 685 | maxVal = max(maxVal, poly.z1) 686 | maxVal = max(maxVal, poly.z2) 687 | maxVal = max(maxVal, poly.z3) 688 | end 689 | 690 | for i = 1, #model do 691 | local poly = model[i] 692 | poly.x1 = poly.x1 / maxVal 693 | poly.x2 = poly.x2 / maxVal 694 | poly.x3 = poly.x3 / maxVal 695 | poly.y1 = poly.y1 / maxVal 696 | poly.y2 = poly.y2 / maxVal 697 | poly.y3 = poly.y3 / maxVal 698 | poly.z1 = poly.z1 / maxVal 699 | poly.z2 = poly.z2 / maxVal 700 | poly.z3 = poly.z3 / maxVal 701 | end 702 | 703 | return model 704 | end 705 | 706 | ---Similar to normalizeScale, rescales the model, but only uses the y coordinate to determine how much it is scaled (normalizes height) 707 | ---@param model Model 708 | function transforms.normalizeScaleY(model) 709 | local maxVal = -math.huge 710 | 711 | for i = 1, #model do 712 | local poly = model[i] 713 | maxVal = max(maxVal, poly.y1) 714 | maxVal = max(maxVal, poly.y2) 715 | maxVal = max(maxVal, poly.y3) 716 | end 717 | 718 | for i = 1, #model do 719 | local poly = model[i] 720 | poly.x1 = poly.x1 / maxVal 721 | poly.x2 = poly.x2 / maxVal 722 | poly.x3 = poly.x3 / maxVal 723 | poly.y1 = poly.y1 / maxVal 724 | poly.y2 = poly.y2 / maxVal 725 | poly.y3 = poly.y3 / maxVal 726 | poly.z1 = poly.z1 / maxVal 727 | poly.z2 = poly.z2 / maxVal 728 | poly.z3 = poly.z3 / maxVal 729 | end 730 | 731 | return model 732 | end 733 | 734 | ---Scales the model 735 | ---@param model Model 736 | ---@param scale number 737 | function transforms.scale(model, scale) 738 | for i = 1, #model do 739 | local poly = model[i] 740 | poly.x1 = poly.x1 * scale 741 | poly.x2 = poly.x2 * scale 742 | poly.x3 = poly.x3 * scale 743 | poly.y1 = poly.y1 * scale 744 | poly.y2 = poly.y2 * scale 745 | poly.y3 = poly.y3 * scale 746 | poly.z1 = poly.z1 * scale 747 | poly.z2 = poly.z2 * scale 748 | poly.z3 = poly.z3 * scale 749 | end 750 | 751 | return model 752 | end 753 | 754 | ---Translates the model 755 | ---@param model Model 756 | ---@param dx number? 757 | ---@param dy number? 758 | ---@param dz number? 759 | function transforms.translate(model, dx, dy, dz) 760 | for i = 1, #model do 761 | local poly = model[i] 762 | poly.x1 = poly.x1 + (dx or 0) 763 | poly.x2 = poly.x2 + (dx or 0) 764 | poly.x3 = poly.x3 + (dx or 0) 765 | poly.y1 = poly.y1 + (dy or 0) 766 | poly.y2 = poly.y2 + (dy or 0) 767 | poly.y3 = poly.y3 + (dy or 0) 768 | poly.z1 = poly.z1 + (dz or 0) 769 | poly.z2 = poly.z2 + (dz or 0) 770 | poly.z3 = poly.z3 + (dz or 0) 771 | end 772 | 773 | return model 774 | end 775 | 776 | ---Rotates a given Model around three axes 777 | ---@param model Model 778 | ---@param rotX number? in radians 779 | ---@param rotY number? in radians 780 | ---@param rotZ number? in radians 781 | ---@return Model 782 | function transforms.rotate(model, rotX, rotY, rotZ) 783 | local rotXS, rotXC = 0, 1 784 | local rotYS, rotYC = 0, 1 785 | local rotZS, rotZC = 0, 1 786 | 787 | if rotX == 0 then rotX = nil end 788 | if rotX then rotXS, rotXC = sin(rotX), cos(rotX) end 789 | if rotY == 0 then rotY = nil end 790 | if rotY then rotYS, rotYC = sin(rotY), cos(rotY) end 791 | if rotZ == 0 then rotZ = nil end 792 | if rotZ then rotZS, rotZC = sin(rotZ), cos(rotZ) end 793 | 794 | for i = 1, #model do 795 | local polygon = model[i] 796 | 797 | local x1, y1, z1 = polygon.x1, polygon.y1, polygon.z1 798 | local x2, y2, z2 = polygon.x2, polygon.y2, polygon.z2 799 | local x3, y3, z3 = polygon.x3, polygon.y3, polygon.z3 800 | 801 | if rotY then 802 | x1, y1, z1 = rotatePolygonY(x1, y1, z1, rotYS, rotYC) 803 | x2, y2, z2 = rotatePolygonY(x2, y2, z2, rotYS, rotYC) 804 | x3, y3, z3 = rotatePolygonY(x3, y3, z3, rotYS, rotYC) 805 | end 806 | 807 | if rotZ then 808 | x1, y1 = rotatePolygonZ(x1, y1, z1, rotZS, rotZC) 809 | x2, y2 = rotatePolygonZ(x2, y2, z2, rotZS, rotZC) 810 | x3, y3 = rotatePolygonZ(x3, y3, z3, rotZS, rotZC) 811 | end 812 | 813 | if rotX then 814 | x1, y1, z1 = rotatePolygonX(x1, y1, z1, rotXS, rotXC) 815 | x2, y2, z2 = rotatePolygonX(x2, y2, z2, rotXS, rotXC) 816 | x3, y3, z3 = rotatePolygonX(x3, y3, z3, rotXS, rotXC) 817 | end 818 | 819 | polygon.x1, polygon.y1, polygon.z1 = x1, y1, z1 820 | polygon.x2, polygon.y2, polygon.z2 = x2, y2, z2 821 | polygon.x3, polygon.y3, polygon.z3 = x3, y3, z3 822 | end 823 | 824 | return model 825 | end 826 | 827 | ---Translates the model such that the bottom aligns with y = 0 828 | ---@param model Model 829 | function transforms.alignBottom(model) 830 | local minY = math.huge 831 | 832 | for i = 1, #model do 833 | local poly = model[i] 834 | minY = min(minY, poly.y1) 835 | minY = min(minY, poly.y2) 836 | minY = min(minY, poly.y3) 837 | end 838 | 839 | for i = 1, #model do 840 | local poly = model[i] 841 | poly.y1 = poly.y1 - minY 842 | poly.y2 = poly.y2 - minY 843 | poly.y3 = poly.y3 - minY 844 | end 845 | 846 | return model 847 | end 848 | 849 | ---Triangle decimation (reduce the quality / number of polygons in a model), default mode "ratio" 850 | ---@param model Model 851 | ---@param quality number 852 | ---@param mode? "ratio"|"polys" 853 | ---@return Model 854 | function transforms.decimate(model, quality, mode) 855 | -- convert model format 856 | 857 | local vertices = {} 858 | local triangles = {} 859 | 860 | local function findVertex(x, y, z) 861 | for i = #vertices, 1, -1 do 862 | local vertex = vertices[i] 863 | if vertex[1] == x and vertex[2] == y and vertex[3] == z then 864 | return i 865 | end 866 | end 867 | end 868 | 869 | for i = 1, #model do 870 | local poly = model[i] 871 | 872 | local i1 = findVertex(poly.x1, poly.y1, poly.z1) 873 | if not i1 then 874 | vertices[#vertices + 1] = {poly.x1, poly.y1, poly.z1} 875 | i1 = #vertices 876 | end 877 | 878 | local i2 = findVertex(poly.x2, poly.y2, poly.z2) 879 | if not i2 then 880 | vertices[#vertices + 1] = {poly.x2, poly.y2, poly.z2} 881 | i2 = #vertices 882 | end 883 | 884 | local i3 = findVertex(poly.x3, poly.y3, poly.z3) 885 | if not i3 then 886 | vertices[#vertices + 1] = {poly.x3, poly.y3, poly.z3} 887 | i3 = #vertices 888 | end 889 | 890 | local triangle = {i1, i2, i3, poly.c, poly.forceRender} 891 | triangles[#triangles + 1] = triangle 892 | end 893 | 894 | -- actually decimate 895 | 896 | local function getDistance(v1, v2) 897 | local dx = v1[1] - v2[1] 898 | local dy = v1[2] - v2[2] 899 | local dz = v1[3] - v2[3] 900 | return (dx * dx + dy * dy + dz * dz) ^ 0.5 901 | end 902 | 903 | ---@class EdgeCandidate 904 | ---@field length number 905 | ---@field vA number vertex index 906 | ---@field vB number vertex index 907 | 908 | ---@type EdgeCandidate[] 909 | local candidateEdges = {} 910 | for i = 1, #triangles do 911 | local triangle = triangles[i] 912 | local edge1Length = getDistance( 913 | vertices[triangle[1]], 914 | vertices[triangle[2]] 915 | ) 916 | local edge2Length = getDistance( 917 | vertices[triangle[2]], 918 | vertices[triangle[3]] 919 | ) 920 | local edge3Length = getDistance( 921 | vertices[triangle[1]], 922 | vertices[triangle[3]] 923 | ) 924 | 925 | candidateEdges[#candidateEdges + 1] = { 926 | length = edge1Length, 927 | vA = triangle[1], 928 | vB = triangle[2], 929 | } 930 | 931 | candidateEdges[#candidateEdges + 1] = { 932 | length = edge2Length, 933 | vA = triangle[2], 934 | vB = triangle[3], 935 | } 936 | 937 | candidateEdges[#candidateEdges + 1] = { 938 | length = edge3Length, 939 | vA = triangle[1], 940 | vB = triangle[3], 941 | } 942 | end 943 | 944 | local function collapseEdge(vA, vB) 945 | local vertex1 = vertices[vA] 946 | local vertex2 = vertices[vB] 947 | local vertexNew = { -- TODO: Maybe don't use average position, but offset in a smart way? 948 | (vertex1[1] + vertex2[1]) * 0.5, 949 | (vertex1[2] + vertex2[2]) * 0.5, 950 | (vertex1[3] + vertex2[3]) * 0.5, 951 | } 952 | vertices[#vertices + 1] = vertexNew 953 | local vNew = #vertices 954 | 955 | for i = #triangles, 1, -1 do 956 | local tri = triangles[i] 957 | if tri[1] == vA or tri[1] == vB or tri[2] == vA or tri[2] == vB or tri[3] == vA or tri[3] == vB then 958 | -- triangle contains at least one of the vertices being collapsed 959 | 960 | if tri[1] == vA or tri[1] == vB then 961 | tri[1] = vNew 962 | end 963 | if tri[2] == vA or tri[2] == vB then 964 | tri[2] = vNew 965 | end 966 | if tri[3] == vA or tri[3] == vB then 967 | tri[3] = vNew 968 | end 969 | 970 | if tri[1] == tri[2] or tri[2] == tri[3] or tri[1] == tri[3] then 971 | -- no longer valid triangle 972 | table.remove(triangles, i) 973 | end 974 | end 975 | end 976 | 977 | return vNew 978 | end 979 | 980 | local maxTriangles = quality 981 | if mode ~= "polys" then 982 | maxTriangles = (#model) * quality 983 | end 984 | while #triangles > maxTriangles do 985 | -- find smallest edge 986 | local smallestLength = math.huge 987 | local smallestIndex 988 | for i = 1, #candidateEdges do 989 | local candidate = candidateEdges[i] 990 | if candidate.length < smallestLength then 991 | smallestLength = candidate.length 992 | smallestIndex = i 993 | end 994 | end 995 | local smallestCandidate = candidateEdges[smallestIndex] 996 | 997 | -- collapse smallest edge 998 | local vNew = collapseEdge(smallestCandidate.vA, smallestCandidate.vB) 999 | 1000 | -- remove now invalid candidate edges 1001 | for i = #candidateEdges, 1, -1 do 1002 | local candidate = candidateEdges[i] 1003 | local replaced1 = candidate.vA == smallestCandidate.vA or candidate.vA == smallestCandidate.vB 1004 | local replaced2 = candidate.vB == smallestCandidate.vA or candidate.vB == smallestCandidate.vB 1005 | if replaced1 and replaced2 then 1006 | table.remove(candidateEdges, i) 1007 | elseif replaced1 then 1008 | candidate.vA = vNew 1009 | elseif replaced2 then 1010 | candidate.vB = vNew 1011 | end 1012 | end 1013 | end 1014 | 1015 | -- build new model 1016 | ---@type Model 1017 | local newModel = {} 1018 | 1019 | for i = 1, #triangles do 1020 | local triangle = triangles[i] 1021 | local v1 = vertices[triangle[1]] 1022 | local v2 = vertices[triangle[2]] 1023 | local v3 = vertices[triangle[3]] 1024 | 1025 | newModel[#newModel + 1] = { 1026 | x1 = v1[1], 1027 | y1 = v1[2], 1028 | z1 = v1[3], 1029 | x2 = v2[1], 1030 | y2 = v2[2], 1031 | z2 = v2[3], 1032 | x3 = v3[1], 1033 | y3 = v3[2], 1034 | z3 = v3[3], 1035 | c = triangle[4], 1036 | forceRender = triangle[5], 1037 | } 1038 | end 1039 | 1040 | for name, func in pairs(transforms) do 1041 | newModel[name] = func 1042 | end 1043 | 1044 | return newModel 1045 | end 1046 | 1047 | local loadModel, loadModelRaw 1048 | 1049 | ---Create a new Level of Detail Model 1050 | ---@param settings {minQuality: number?, variantCount: number?, qualityHalvingDistance: number?, quickInitWorseRuntime: boolean?}? 1051 | ---@return LoDModel 1052 | function transforms.toLoD(model, settings) 1053 | settings = settings or {} 1054 | 1055 | ---@class LoDModel 1056 | local LoDModel = { 1057 | minQuality = settings.minQuality or 0.1, 1058 | variantCount = settings.variantCount or 4, 1059 | qualityHalvingDistance = settings.qualityHalvingDistance or 5, 1060 | quickInitWorseRuntime = settings.quickInitWorseRuntime or false, 1061 | } 1062 | 1063 | local objModel, modelSize = loadModelRaw(model) 1064 | 1065 | ---@type LoDVariant[] 1066 | local modelVariants = { 1067 | { 1068 | quality = 1, 1069 | collapsedModel = objModel, 1070 | size = modelSize, 1071 | } 1072 | } 1073 | 1074 | local prevQuality = model 1075 | local originalPolyCount = #prevQuality 1076 | 1077 | for i = 1, LoDModel.variantCount do 1078 | local quality = (1 - LoDModel.minQuality) * (1 - i / LoDModel.variantCount) + LoDModel.minQuality 1079 | 1080 | local polyCount = math.floor(originalPolyCount * quality + 0.5) 1081 | local decreasedQualityModel = prevQuality:decimate(polyCount, "polys") 1082 | local objModel, modelSize = loadModelRaw(decreasedQualityModel) 1083 | prevQuality = decreasedQualityModel 1084 | 1085 | ---@class LoDVariant 1086 | local var = { 1087 | quality = quality, 1088 | collapsedModel = objModel, 1089 | size = modelSize, 1090 | } 1091 | 1092 | modelVariants[#modelVariants + 1] = var 1093 | end 1094 | 1095 | local function copyVariants(vars) 1096 | local newVars = {} 1097 | for i = 1, #vars do 1098 | local var = vars[i] 1099 | local model = var.collapsedModel 1100 | 1101 | local modelNew = {} 1102 | for j = 1, #model do 1103 | local poly = model[j] 1104 | local polyNew = { 1105 | poly[1], 1106 | poly[2], 1107 | poly[3], 1108 | poly[4], 1109 | poly[5], 1110 | poly[6], 1111 | poly[7], 1112 | poly[8], 1113 | poly[9], 1114 | poly[10], 1115 | poly[11], 1116 | poly[12], 1117 | } 1118 | modelNew[j] = polyNew 1119 | end 1120 | 1121 | local varNew = { 1122 | quality = var.quality, 1123 | size = var.size, 1124 | collapsedModel = modelNew, 1125 | } 1126 | 1127 | newVars[i] = varNew 1128 | end 1129 | return newVars 1130 | end 1131 | 1132 | ---@type LoDVariant[][] 1133 | local objectVariants = {} 1134 | 1135 | ---[INTERNAL] initialize new PineObject 1136 | ---@param object PineObject 1137 | function LoDModel:initObject(object) 1138 | local variants 1139 | if LoDModel.quickInitWorseRuntime then 1140 | variants = modelVariants 1141 | else 1142 | variants = copyVariants(modelVariants) 1143 | end 1144 | objectVariants[object] = variants 1145 | 1146 | object[7] = variants[1].collapsedModel 1147 | object[8] = variants[1].size 1148 | object[9] = LoDModel 1149 | end 1150 | 1151 | ---Update the LoD for a table of objects with respect to a camera position 1152 | ---@param object PineObject 1153 | ---@param camera CollapsedCamera 1154 | function LoDModel:update(object, camera) 1155 | local variants = objectVariants[object] 1156 | 1157 | if variants then 1158 | local dx = camera[1] - object[1] 1159 | local dy = camera[2] - object[2] 1160 | local dz = camera[3] - object[3] 1161 | local d = (dx * dx + dy * dy + dz * dz) ^ 0.5 1162 | local targetQuality = math.min(1, math.max(LoDModel.minQuality, 1 / (d / LoDModel.qualityHalvingDistance))) 1163 | local closestRaw = (LoDModel.variantCount + 1) - 1164 | LoDModel.variantCount * (targetQuality - LoDModel.minQuality) / (1 - LoDModel.minQuality) 1165 | local closest = math.floor(closestRaw + 0.5) 1166 | 1167 | local var = variants[closest] 1168 | object[7] = var.collapsedModel 1169 | object[8] = var.size 1170 | end 1171 | end 1172 | 1173 | return LoDModel 1174 | end 1175 | 1176 | local sqrt = math.sqrt 1177 | ---Used internally. Creates a CollapsedModel from a Model and also returns the biggest distance from its origin (for frustum culling) 1178 | ---@param model Model 1179 | ---@return CollapsedModel 1180 | ---@return number biggestDistance largest distance of a vertex in the model from its origin 1181 | function loadModelRaw(model) 1182 | ---@class CollapsedModel 1183 | local transformedModel = {} 1184 | local biggestDistance = 0 1185 | 1186 | for i = 1, #model do 1187 | local polygon = model[i] 1188 | transformedModel[#transformedModel + 1] = {} 1189 | transformedModel[#transformedModel][1] = polygon.x1 1190 | transformedModel[#transformedModel][2] = polygon.y1 1191 | transformedModel[#transformedModel][3] = polygon.z1 1192 | transformedModel[#transformedModel][4] = polygon.x2 1193 | transformedModel[#transformedModel][5] = polygon.y2 1194 | transformedModel[#transformedModel][6] = polygon.z2 1195 | transformedModel[#transformedModel][7] = polygon.x3 1196 | transformedModel[#transformedModel][8] = polygon.y3 1197 | transformedModel[#transformedModel][9] = polygon.z3 1198 | transformedModel[#transformedModel][10] = polygon.forceRender 1199 | transformedModel[#transformedModel][11] = polygon.c 1200 | transformedModel[#transformedModel][12] = polygon.outlineColor 1201 | 1202 | local d1 = sqrt(polygon.x1 * polygon.x1 + polygon.y1 * polygon.y1 + polygon.z1 * polygon.z1) 1203 | local d2 = sqrt(polygon.x2 * polygon.x2 + polygon.y2 * polygon.y2 + polygon.z2 * polygon.z2) 1204 | local d3 = sqrt(polygon.x3 * polygon.x3 + polygon.y3 * polygon.y3 + polygon.z3 * polygon.z3) 1205 | 1206 | if d1 > biggestDistance then 1207 | biggestDistance = d1 1208 | end 1209 | if d2 > biggestDistance then 1210 | biggestDistance = d2 1211 | end 1212 | if d3 > biggestDistance then 1213 | biggestDistance = d3 1214 | end 1215 | end 1216 | return transformedModel, biggestDistance 1217 | end 1218 | 1219 | ---@param path string 1220 | function loadModel(path) 1221 | local modelFile = fs.open(path, "r") 1222 | if not modelFile then 1223 | error("Could not find model for an object at path: " .. path) 1224 | end 1225 | local content = modelFile.readAll() 1226 | modelFile.close() 1227 | if not content then 1228 | error("Failed to parse model for an object at path: " .. path) 1229 | end 1230 | 1231 | ---@class Model 1232 | ---@field invertTriangles fun(self: Model): Model Inverts the direction of all triangles 1233 | ---@field setOutline fun(self: Model, col: number|table): Model Change the outline colors of polygons in a Model. If number, will set the outline color for each Polygon, if table, uses it as a mapping from Polygon color to new outline color 1234 | ---@field mapColor fun(self: Model, col: number|table): Model Change the colors of polygons in a Model. If number, will set the color for each Polygon, if table, uses it as a mapping from Polygon color to new the new color 1235 | ---@field center fun(self: Model): Model Center the Model such that the origin is in the middle of the bounding box 1236 | ---@field normalizeScale fun(self: Model): Model Rescales the model such that the largest value of any coordinate is equal to 1 1237 | ---@field normalizeScaleY fun(self: Model): Model Similar to normalizeScale, rescales the model, but only uses the y coordinate to determine how much it is scaled (normalizes height) 1238 | ---@field scale fun(self: Model, scale: number): Model Scales the model 1239 | ---@field translate fun(self: Model, dx: number?, dy: number?, dz: number?): Model Translates the model 1240 | ---@field rotate fun(self: Model, rotX: number?, rotY: number?, rotZ: number?): Model Rotates a given Model around three axes (radians) 1241 | ---@field alignBottom fun(self: Model): Model Translates the model such that the bottom aligns with y = 0 1242 | ---@field decimate fun(self: Model, quality: number, mode?: "ratio"|"polys"): Model Triangle decimation (reduce the quality / number of polygons in a model) 1243 | ---@field toLoD fun(self: Model, settings: {minQuality: number?, variantCount: number?, qualityHalvingDistance: number?, quickInitWorseRuntime: boolean?}?): LoDModel Create a new Level of Detail Model 1244 | local model = textutils.unserialise(content) 1245 | 1246 | for name, func in pairs(transforms) do 1247 | model[name] = func 1248 | end 1249 | 1250 | return model 1251 | end 1252 | 1253 | local pi = math.pi 1254 | 1255 | local sin = math.sin 1256 | local cos = math.cos 1257 | local tan = math.tan 1258 | local sqrt = math.sqrt 1259 | 1260 | ---Creates a new ThreeDFrame 1261 | ---@param x integer? the new x position of the ThreeDFrame on the screen 1262 | ---@param y integer? the new y position of the ThreeDFrame on the screen 1263 | ---@param w integer? the new width of the ThreeDFrame on the screen 1264 | ---@param h integer? the new height of the ThreeDFrame on the screen 1265 | ---@return ThreeDFrame 1266 | local function newFrame(x, y, w, h) 1267 | local term_width, term_height = term.getSize() 1268 | x = x or 1 1269 | y = y or 1 1270 | w = w or term_width 1271 | h = h or term_height 1272 | 1273 | ---@class ThreeDFrame 1274 | local frame = { 1275 | ---@class CollapsedCamera 1276 | camera = { 1277 | 0, 1278 | 0, 1279 | 0, 1280 | nil, 1281 | 0, 1282 | 0, 1283 | }, 1284 | buffer = newBuffer(x, y, w, h), 1285 | width = w * 2, 1286 | height = h * 3, 1287 | } 1288 | frame.FoV = 90 1289 | frame.camera[7] = rad(frame.FoV) 1290 | frame.camera[8] = 2 * math.atan(math.tan(rad(frame.FoV) * 0.5) / (frame.width / frame.height / 1.5)) 1291 | frame.t = tan(rad(frame.FoV / 2)) * 2 * 0.0001 1292 | 1293 | local renderOffsetX, renderOffsetY, sXFactor, sYFactor 1294 | 1295 | function frame:setBackgroundColor(c) 1296 | local buff = self.buffer 1297 | ---@diagnostic disable-next-line: inject-field 1298 | buff.backgroundColor = c 1299 | buff:fastClear() 1300 | end 1301 | 1302 | local function updateMappingConstants() 1303 | renderOffsetX = floor(frame.width * 0.5) + 1 1304 | renderOffsetY = floor(frame.height * 0.5) 1305 | 1306 | sXFactor = 0.0001 * frame.width / frame.t 1307 | sYFactor = -0.0001 * frame.width / (frame.t * frame.height) * frame.height 1308 | end 1309 | updateMappingConstants() 1310 | 1311 | function frame:setSize(x, y, w, h) 1312 | self.width = w * 2 1313 | self.height = h * 3 1314 | self.buffer:setSize(x, y, w, h) 1315 | updateMappingConstants() 1316 | end 1317 | 1318 | ---If enabled, uses depth interpolation (enabled by default) 1319 | ---@param enabled boolean 1320 | function frame:depthInterpolation(enabled) 1321 | self.interpolateDepth = enabled 1322 | self.buffer:depthInterpolation(enabled) 1323 | end 1324 | 1325 | function frame:map3dTo2d(x, y, z) 1326 | local camera = self.camera 1327 | local cA1 = sin(camera[4] or 0) 1328 | local cA2 = cos(camera[4] or 0) 1329 | local cA3 = sin(-camera[5]) 1330 | local cA4 = cos(-camera[5]) 1331 | local cA5 = sin(camera[6]) 1332 | local cA6 = cos(camera[6]) 1333 | 1334 | local dX = x - camera[1] 1335 | local dY = y - camera[2] 1336 | local dZ = z - camera[3] 1337 | 1338 | local dX2 = cA4 * dX - cA3 * dZ 1339 | dZ = cA3 * dX + cA4 * dZ 1340 | dX = dX2 1341 | 1342 | local dY2 = cA6 * dY - cA5 * dX 1343 | dX = cA5 * dY + cA6 * dX 1344 | dY = dY2 1345 | 1346 | if cA1 ~= 0 then 1347 | local dZ2 = cA1 * dZ - cA2 * dY 1348 | dY = cA2 * dZ + cA1 * dY 1349 | dZ = dZ2 1350 | end 1351 | 1352 | local sX = (dZ / dX) * sXFactor + renderOffsetX 1353 | local sY = (dY / dX) * sYFactor + renderOffsetY 1354 | 1355 | return sX, sY, dX >= 0.0001 1356 | end 1357 | 1358 | ---Draw an object 1359 | ---@param object PineObject 1360 | ---@param camera CollapsedCamera 1361 | ---@param cameraAngles CameraAngles 1362 | function frame:drawObject(object, camera, cameraAngles) 1363 | local oX = object[1] 1364 | local oY = object[2] 1365 | local oZ = object[3] 1366 | 1367 | local cA1 = cameraAngles[1] 1368 | local cA2 = cameraAngles[2] 1369 | local cA3 = cameraAngles[3] 1370 | local cA4 = cameraAngles[4] 1371 | local cA5 = cameraAngles[5] 1372 | local cA6 = cameraAngles[6] 1373 | 1374 | local xCameraOffset = oX and (oX - camera[1]) or 0 1375 | local yCameraOffset = oY and (oY - camera[2]) or 0 1376 | local zCameraOffset = oZ and (oZ - camera[3]) or 0 1377 | 1378 | local LoDModel = object[9] 1379 | if LoDModel then 1380 | LoDModel:update(object, camera) 1381 | end 1382 | 1383 | local model = object[7] 1384 | if #model <= 0 then 1385 | return 1386 | end 1387 | local modelSize = object[8] 1388 | 1389 | local dX = xCameraOffset 1390 | local dY = yCameraOffset 1391 | local dZ = zCameraOffset 1392 | 1393 | local dX2 = cA4 * dX - cA3 * dZ 1394 | local dZ = cA3 * dX + cA4 * dZ 1395 | local dX = dX2 1396 | 1397 | local dY2 = cA6 * dY - cA5 * dX 1398 | local dX = cA5 * dY + cA6 * dX 1399 | dY = dY2 1400 | 1401 | -- frustum culling behind camera (near) 1402 | if dX < -modelSize then 1403 | return true 1404 | end 1405 | 1406 | -- frustum culling left/right 1407 | local FoV = 0.5 * camera[7] 1408 | local dotX = sin(FoV) 1409 | local dotZ = cos(FoV) 1410 | 1411 | if (dX + modelSize) * dotX + (dZ + modelSize) * dotZ < 0 then 1412 | return true 1413 | end 1414 | if (dX + modelSize) * dotX - (dZ - modelSize) * dotZ < 0 then 1415 | return true 1416 | end 1417 | 1418 | -- frustum culling top/bottom 1419 | local verticalFOV = 0.5 * camera[8] 1420 | local dotX = sin(verticalFOV) 1421 | local dotY = cos(verticalFOV) 1422 | 1423 | if (dX + modelSize) * dotX + (dY + modelSize) * dotY < 0 then 1424 | return true 1425 | end 1426 | if (dX + modelSize) * dotX - (dY - modelSize) * dotY < 0 then 1427 | return true 1428 | end 1429 | 1430 | local rotX = object[4] 1431 | local rotY = object[5] 1432 | local rotZ = object[6] 1433 | if (rotX and rotX ~= 0) or (rotY and rotY ~= 0) or (rotZ and rotZ ~= 0) then 1434 | model = rotateCollapsedModel(model, rotX, rotY, rotZ) 1435 | end 1436 | computePolyCamDistance(model, oX, oY, oZ, camera) 1437 | 1438 | local clippingEnabled = xCameraOffset * xCameraOffset + yCameraOffset * yCameraOffset + zCameraOffset * zCameraOffset < 1439 | modelSize * modelSize * 4 1440 | 1441 | local renderOffsetX = renderOffsetX 1442 | local renderOffsetY = renderOffsetY 1443 | 1444 | local sXFactor = sXFactor 1445 | local sYFactor = sYFactor 1446 | 1447 | local cA1 = cA1 1448 | local cA2 = cA2 1449 | local cA3 = cA3 1450 | local cA4 = cA4 1451 | local cA5 = cA5 1452 | local cA6 = cA6 1453 | 1454 | local xCameraOffset = xCameraOffset 1455 | local yCameraOffset = yCameraOffset 1456 | local zCameraOffset = zCameraOffset 1457 | 1458 | local function map3dTo2d(dX, dY, dZ) 1459 | local dX2 = cA4 * dX - cA3 * dZ 1460 | dZ = cA3 * dX + cA4 * dZ 1461 | dX = dX2 1462 | 1463 | local dY2 = cA6 * dY - cA5 * dX 1464 | dX = cA5 * dY + cA6 * dX 1465 | dY = dY2 1466 | 1467 | local sX = (dZ / dX) * sXFactor + renderOffsetX 1468 | local sY = (dY / dX) * sYFactor + renderOffsetY 1469 | 1470 | return sX, sY, dX 1471 | end 1472 | 1473 | if cA1 ~= 0 then 1474 | function map3dTo2d(dX, dY, dZ) 1475 | local dX2 = cA4 * dX - cA3 * dZ 1476 | dZ = cA3 * dX + cA4 * dZ 1477 | dX = dX2 1478 | 1479 | local dY2 = cA6 * dY - cA5 * dX 1480 | dX = cA5 * dY + cA6 * dX 1481 | dY = dY2 1482 | 1483 | local dZ2 = cA1 * dZ - cA2 * dY 1484 | dY = cA2 * dZ + cA1 * dY 1485 | dZ = dZ2 1486 | 1487 | local sX = (dZ / dX) * sXFactor + renderOffsetX 1488 | local sY = (dY / dX) * sYFactor + renderOffsetY 1489 | 1490 | return sX, sY, dX 1491 | end 1492 | end 1493 | 1494 | local depthPolygons = model 1495 | local buff = self.buffer 1496 | for i = 1, #depthPolygons do 1497 | local polygon = depthPolygons[i] 1498 | local depth = polygon[16] 1499 | 1500 | local x1, y1, dX1 = map3dTo2d(polygon[1] + xCameraOffset, polygon[2] + yCameraOffset, polygon[3] + zCameraOffset) 1501 | if dX1 > 0.00010000001 then 1502 | local x2, y2, dX2 = map3dTo2d(polygon[4] + xCameraOffset, polygon[5] + yCameraOffset, polygon[6] + zCameraOffset) 1503 | if dX2 > 0.00010000001 then 1504 | local x3, y3, dX3 = map3dTo2d(polygon[7] + xCameraOffset, polygon[8] + yCameraOffset, polygon[9] + zCameraOffset) 1505 | if dX3 > 0.00010000001 then 1506 | if polygon[10] or (x2 - x1) * (y3 - y2) - (y2 - y1) * (x3 - x2) < 0 then 1507 | buff:drawTriangle(x1, y1, x2, y2, x3, y3, polygon[11], polygon[12], depth, dX1, dX2, dX3) 1508 | end 1509 | elseif clippingEnabled then 1510 | local function map3dTo2dFull(x, y, z) 1511 | local dX = x + xCameraOffset 1512 | local dY = y + yCameraOffset 1513 | local dZ = z + zCameraOffset 1514 | 1515 | local dX2 = cA4 * dX - cA3 * dZ 1516 | dZ = cA3 * dX + cA4 * dZ 1517 | dX = dX2 1518 | 1519 | local dY2 = cA6 * dY - cA5 * dX 1520 | dX = cA5 * dY + cA6 * dX 1521 | dY = dY2 1522 | 1523 | if cA1 ~= 0 then 1524 | local dZ2 = cA1 * dZ - cA2 * dY 1525 | dY = cA2 * dZ + cA1 * dY 1526 | dZ = dZ2 1527 | end 1528 | 1529 | local sX = (dZ / dX) * sXFactor + renderOffsetX 1530 | local sY = (dY / dX) * sYFactor + renderOffsetY 1531 | 1532 | return sX, sY, dX, dY, dZ 1533 | end 1534 | 1535 | local x1, y1, dX1, dY1, dZ1 = map3dTo2dFull(polygon[1], polygon[2], polygon[3]) 1536 | local x2, y2, dX2, dY2, dZ2 = map3dTo2dFull(polygon[4], polygon[5], polygon[6]) 1537 | local x3, y3, dX3, dY3, dZ3 = map3dTo2dFull(polygon[7], polygon[8], polygon[9]) 1538 | 1539 | local abs = math.abs 1540 | 1541 | -- 1, 1, 0 1542 | 1543 | local w3 = abs(dX3 - 0.0001) 1544 | local w1 = abs(dX1 - 0.0001) 1545 | local wT = w1 + w3 1546 | local newPosAZ = (dZ3 * w1 + dZ1 * w3) / wT 1547 | local newPosAY = (dY3 * w1 + dY1 * w3) / wT 1548 | 1549 | local renderOffsetX, sXFactor, renderOffsetY, sYFactor = renderOffsetX, sXFactor, renderOffsetY, sYFactor 1550 | local AX = (newPosAZ * 10000) * sXFactor + renderOffsetX 1551 | local AY = (newPosAY * 10000) * sYFactor + renderOffsetY 1552 | 1553 | if polygon[10] or (x2 - x1) * (AY - y2) - (y2 - y1) * (AX - x2) < 0 then 1554 | buff:drawTriangle(x1, y1, x2, y2, AX, AY, polygon[11], polygon[12], depth, dX1, dX2, 0.0001) 1555 | 1556 | local w2 = abs(dX2 - 0.0001) 1557 | local wT = w2 + w3 1558 | local newPosAZ = (dZ2 * w3 + dZ3 * w2) / wT 1559 | local newPosAY = (dY2 * w3 + dY3 * w2) / wT 1560 | 1561 | local BX = (newPosAZ * 10000) * sXFactor + renderOffsetX 1562 | local BY = (newPosAY * 10000) * sYFactor + renderOffsetY 1563 | buff:drawTriangle(BX, BY, x2, y2, AX, AY, polygon[11], polygon[12], depth, 0.0001, dX2, 0.0001) 1564 | end 1565 | end 1566 | elseif clippingEnabled then 1567 | local function map3dTo2dFull(x, y, z) 1568 | local dX = x + xCameraOffset 1569 | local dY = y + yCameraOffset 1570 | local dZ = z + zCameraOffset 1571 | 1572 | local dX2 = cA4 * dX - cA3 * dZ 1573 | dZ = cA3 * dX + cA4 * dZ 1574 | dX = dX2 1575 | 1576 | local dY2 = cA6 * dY - cA5 * dX 1577 | dX = cA5 * dY + cA6 * dX 1578 | dY = dY2 1579 | 1580 | if cA1 ~= 0 then 1581 | local dZ2 = cA1 * dZ - cA2 * dY 1582 | dY = cA2 * dZ + cA1 * dY 1583 | dZ = dZ2 1584 | end 1585 | 1586 | local sX = (dZ / dX) * sXFactor + renderOffsetX 1587 | local sY = (dY / dX) * sYFactor + renderOffsetY 1588 | 1589 | return sX, sY, dX, dY, dZ 1590 | end 1591 | 1592 | local x1, y1, dX1, dY1, dZ1 = map3dTo2dFull(polygon[1], polygon[2], polygon[3]) 1593 | local x2, y2, dX2, dY2, dZ2 = map3dTo2dFull(polygon[4], polygon[5], polygon[6]) 1594 | local x3, y3, dX3, dY3, dZ3 = map3dTo2dFull(polygon[7], polygon[8], polygon[9]) 1595 | 1596 | local abs = math.abs 1597 | 1598 | if dX3 > 0.00010000001 then 1599 | -- 1 0 1 1600 | 1601 | local w2 = abs(dX2 - 0.0001) 1602 | local w1 = abs(dX1 - 0.0001) 1603 | local wT = w1 + w2 1604 | local newPosAZ = (dZ2 * w1 + dZ1 * w2) / wT 1605 | local newPosAY = (dY2 * w1 + dY1 * w2) / wT 1606 | 1607 | local renderOffsetX, sXFactor, renderOffsetY, sYFactor = renderOffsetX, sXFactor, renderOffsetY, sYFactor 1608 | local AX = (newPosAZ * 10000) * sXFactor + renderOffsetX 1609 | local AY = (newPosAY * 10000) * sYFactor + renderOffsetY 1610 | 1611 | if polygon[10] or (AX - x1) * (y3 - AY) - (AY - y1) * (x3 - AX) < 0 then 1612 | buff:drawTriangle(x1, y1, AX, AY, x3, y3, polygon[11], polygon[12], depth, dX1, 0.0001, dX3) 1613 | 1614 | local w3 = abs(dX3 - 0.0001) 1615 | local wT = w2 + w3 1616 | local newPosAZ = (dZ2 * w3 + dZ3 * w2) / wT 1617 | local newPosAY = (dY2 * w3 + dY3 * w2) / wT 1618 | 1619 | local BX = (newPosAZ * 10000) * sXFactor + renderOffsetX 1620 | local BY = (newPosAY * 10000) * sYFactor + renderOffsetY 1621 | buff:drawTriangle(BX, BY, AX, AY, x3, y3, polygon[11], polygon[12], depth, 0.0001, 0.0001, dX3) 1622 | end 1623 | else 1624 | -- 1 0 0 1625 | 1626 | local w1 = abs(dX1 - 0.0001) 1627 | local w2 = abs(dX2 - 0.0001) 1628 | local w3 = abs(dX3 - 0.0001) 1629 | local wTA = w1 + w2 1630 | local wTB = w1 + w3 1631 | 1632 | local newPosAZ = (dZ1 * w2 + dZ2 * w1) / wTA 1633 | local newPosAY = (dY1 * w2 + dY2 * w1) / wTA 1634 | local renderOffsetX, sXFactor, renderOffsetY, sYFactor = renderOffsetX, sXFactor, renderOffsetY, sYFactor 1635 | local AX = (newPosAZ * 10000) * sXFactor + renderOffsetX 1636 | local AY = (newPosAY * 10000) * sYFactor + renderOffsetY 1637 | 1638 | local newPosBZ = (dZ1 * w3 + dZ3 * w1) / wTB 1639 | local newPosBY = (dY1 * w3 + dY3 * w1) / wTB 1640 | local BX = (newPosBZ * 10000) * sXFactor + renderOffsetX 1641 | local BY = (newPosBY * 10000) * sYFactor + renderOffsetY 1642 | 1643 | if polygon[10] or (AX - x1) * (BY - AY) - (AY - y1) * (BX - AX) < 0 then 1644 | buff:drawTriangle(x1, y1, AX, AY, BX, BY, polygon[11], polygon[12], depth, dX1, 0.0001, 0.0001) 1645 | end 1646 | end 1647 | end 1648 | elseif clippingEnabled then 1649 | local function map3dTo2dFull(x, y, z) 1650 | local dX = x + xCameraOffset 1651 | local dY = y + yCameraOffset 1652 | local dZ = z + zCameraOffset 1653 | 1654 | local dX2 = cA4 * dX - cA3 * dZ 1655 | dZ = cA3 * dX + cA4 * dZ 1656 | dX = dX2 1657 | 1658 | local dY2 = cA6 * dY - cA5 * dX 1659 | dX = cA5 * dY + cA6 * dX 1660 | dY = dY2 1661 | 1662 | if cA1 ~= 0 then 1663 | local dZ2 = cA1 * dZ - cA2 * dY 1664 | dY = cA2 * dZ + cA1 * dY 1665 | dZ = dZ2 1666 | end 1667 | 1668 | local sX = (dZ / dX) * sXFactor + renderOffsetX 1669 | local sY = (dY / dX) * sYFactor + renderOffsetY 1670 | 1671 | return sX, sY, dX, dY, dZ 1672 | end 1673 | 1674 | local x1, y1, dX1, dY1, dZ1 = map3dTo2dFull(polygon[1], polygon[2], polygon[3]) 1675 | local x2, y2, dX2, dY2, dZ2 = map3dTo2dFull(polygon[4], polygon[5], polygon[6]) 1676 | local x3, y3, dX3, dY3, dZ3 = map3dTo2dFull(polygon[7], polygon[8], polygon[9]) 1677 | 1678 | local abs = math.abs 1679 | 1680 | if dX2 > 0.00010000001 then 1681 | if dX3 > 0.00010000001 then 1682 | -- 0 1 1 1683 | 1684 | local w1 = abs(dX1 - 0.0001) 1685 | local w2 = abs(dX2 - 0.0001) 1686 | local wT = w1 + w2 1687 | local newPosAZ = (dZ1 * w2 + dZ2 * w1) / wT 1688 | local newPosAY = (dY1 * w2 + dY2 * w1) / wT 1689 | 1690 | local renderOffsetX, sXFactor, renderOffsetY, sYFactor = renderOffsetX, sXFactor, renderOffsetY, sYFactor 1691 | local AX = (newPosAZ * 10000) * sXFactor + renderOffsetX 1692 | local AY = (newPosAY * 10000) * sYFactor + renderOffsetY 1693 | if polygon[10] or (x2 - AX) * (y3 - y2) - (y2 - AY) * (x3 - x2) < 0 then 1694 | buff:drawTriangle(AX, AY, x2, y2, x3, y3, polygon[11], polygon[12], depth, 0.0001, dX2, dX3) 1695 | 1696 | local w3 = abs(dX3 - 0.0001) 1697 | local wT = w1 + w3 1698 | local newPosAZ = (dZ1 * w3 + dZ3 * w1) / wT 1699 | local newPosAY = (dY1 * w3 + dY3 * w1) / wT 1700 | 1701 | local BX = (newPosAZ * 10000) * sXFactor + renderOffsetX 1702 | local BY = (newPosAY * 10000) * sYFactor + renderOffsetY 1703 | buff:drawTriangle(AX, AY, BX, BY, x3, y3, polygon[11], polygon[12], depth, 0.0001, 0.0001, dX3) 1704 | end 1705 | else 1706 | -- 0 1 0 1707 | 1708 | local w1 = abs(dX1 - 0.0001) 1709 | local w2 = abs(dX2 - 0.0001) 1710 | local w3 = abs(dX3 - 0.0001) 1711 | local wTA = w2 + w1 1712 | local wTB = w2 + w3 1713 | 1714 | local newPosAZ = (dZ1 * w2 + dZ2 * w1) / wTA 1715 | local newPosAY = (dY1 * w2 + dY2 * w1) / wTA 1716 | local renderOffsetX, sXFactor, renderOffsetY, sYFactor = renderOffsetX, sXFactor, renderOffsetY, sYFactor 1717 | local AX = (newPosAZ * 10000) * sXFactor + renderOffsetX 1718 | local AY = (newPosAY * 10000) * sYFactor + renderOffsetY 1719 | 1720 | local newPosBZ = (dZ2 * w3 + dZ3 * w2) / wTB 1721 | local newPosBY = (dY2 * w3 + dY3 * w2) / wTB 1722 | local BX = (newPosBZ * 10000) * sXFactor + renderOffsetX 1723 | local BY = (newPosBY * 10000) * sYFactor + renderOffsetY 1724 | 1725 | if polygon[10] or (x2 - AX) * (BY - y2) - (y2 - AY) * (BX - x2) < 0 then 1726 | buff:drawTriangle(AX, AY, x2, y2, BX, BY, polygon[11], polygon[12], depth, 0.0001, dX2, 0.0001) 1727 | end 1728 | end 1729 | else 1730 | if dX3 > 0.00010000001 then 1731 | -- 0 0 1 1732 | 1733 | local w1 = abs(dX1 - 0.0001) 1734 | local w2 = abs(dX2 - 0.0001) 1735 | local w3 = abs(dX3 - 0.0001) 1736 | local wTA = w3 + w1 1737 | local wTB = w3 + w2 1738 | 1739 | local newPosAZ = (dZ1 * w3 + dZ3 * w1) / wTA 1740 | local newPosAY = (dY1 * w3 + dY3 * w1) / wTA 1741 | local renderOffsetX, sXFactor, renderOffsetY, sYFactor = renderOffsetX, sXFactor, renderOffsetY, sYFactor 1742 | local AX = (newPosAZ * 10000) * sXFactor + renderOffsetX 1743 | local AY = (newPosAY * 10000) * sYFactor + renderOffsetY 1744 | 1745 | local newPosBZ = (dZ2 * w3 + dZ3 * w2) / wTB 1746 | local newPosBY = (dY2 * w3 + dY3 * w2) / wTB 1747 | local BX = (newPosBZ * 10000) * sXFactor + renderOffsetX 1748 | local BY = (newPosBY * 10000) * sYFactor + renderOffsetY 1749 | 1750 | if polygon[10] or (BX - AX) * (y3 - BY) - (BY - AY) * (x3 - BX) < 0 then 1751 | buff:drawTriangle(AX, AY, BX, BY, x3, y3, polygon[11], polygon[12], depth, 0.0001, 0.0001, dX3) 1752 | end 1753 | --else 1754 | -- 0 0 0 1755 | -- (Don't draw anything) 1756 | end 1757 | end 1758 | end 1759 | end 1760 | end 1761 | 1762 | ---Draw objects and render them to the internal Buffer 1763 | ---@param objects PineObject[] 1764 | function frame:drawObjects(objects) 1765 | self.buffer:clearDepth() 1766 | local camera = self.camera 1767 | ---@class CameraAngles 1768 | local cameraAngles = { 1769 | sin(camera[4] or 0), cos(camera[4] or 0), 1770 | sin(-camera[5]), cos(-camera[5]), 1771 | sin(camera[6]), cos(camera[6]), 1772 | } 1773 | 1774 | local objects = objects 1775 | for i = 1, #objects do 1776 | self:drawObject(objects[i], camera, cameraAngles) 1777 | end 1778 | end 1779 | 1780 | function frame:drawBuffer() 1781 | local buff = self.buffer 1782 | buff:drawBuffer() 1783 | buff:fastClear() 1784 | end 1785 | 1786 | ---@alias PineCamera {x: number?, y: number?, z: number?, rotX: number?, rotY: number?, rotZ: number?} 1787 | 1788 | ---Update any position or rotation value of the Camera. You can also pass a PineCamera 1789 | ---@param cameraX number? 1790 | ---@param cameraY number? 1791 | ---@param cameraZ number? 1792 | ---@param rotX number? 1793 | ---@param rotY number? 1794 | ---@param rotZ number? 1795 | ---@overload fun(self, camera: PineCamera) 1796 | function frame:setCamera(cameraX, cameraY, cameraZ, rotX, rotY, rotZ) 1797 | local rad = math.rad 1798 | if type(cameraX) == "table" then 1799 | local camera = cameraX --[[@as PineCamera]] 1800 | ---@class CollapsedCamera 1801 | self.camera = { 1802 | camera.x or self.camera[1] or 0, 1803 | camera.y or self.camera[2] or 0, 1804 | camera.z or self.camera[3] or 0, 1805 | camera.rotX and rad(camera.rotX + 90) or self.camera[4] or 0, 1806 | camera.rotY and rad(camera.rotY) or self.camera[5] or 0, 1807 | camera.rotZ and rad(camera.rotZ) or self.camera[6] or 0, 1808 | self.camera[7], 1809 | self.camera[8], 1810 | } 1811 | else 1812 | ---@class CollapsedCamera 1813 | self.camera = { 1814 | cameraX or self.camera[1] or 0, 1815 | cameraY or self.camera[2] or 0, 1816 | cameraZ or self.camera[3] or 0, 1817 | rotX and rad(rotX + 90) or self.camera[4] or 0, 1818 | rotY and rad(rotY) or self.camera[5] or 0, 1819 | rotZ and rad(rotZ) or self.camera[6] or 0, 1820 | self.camera[7], 1821 | self.camera[8], 1822 | } 1823 | end 1824 | if self.camera[4] == math.pi * 0.5 then 1825 | self.camera[4] = nil 1826 | end 1827 | end 1828 | 1829 | ---Set the Field of View (degrees) 1830 | ---@param FoV number in degrees 1831 | function frame:setFoV(FoV) 1832 | self.FoV = FoV or 90 1833 | self.t = tan(rad(self.FoV / 2)) * 2 * 0.0001 1834 | updateMappingConstants() 1835 | self.camera[7] = rad(self.FoV) 1836 | self.camera[8] = 2 * math.atan(math.tan(rad(self.FoV) * 0.5) / (self.width / self.height / 1.5)) 1837 | end 1838 | 1839 | ---If set to true, edges of triangles are colored differently to show the wireframe (useful for debugging) 1840 | ---@param enabled boolean 1841 | function frame:setWireFrame(enabled) 1842 | self.buffer:useTriangleEdges(enabled) 1843 | end 1844 | 1845 | ---Get closest object and polygon trace 1846 | ---@param objects PineObject[] 1847 | ---@param x number 1848 | ---@param y number 1849 | ---@return number|nil objectIndex index of a PineObject that was found at the given coordinates in the given table 1850 | ---@return number|nil polyIndex index of the Polygon in the Model of the PineObject if one was found 1851 | ---@return number|nil depth depth from the camera to the Polygon found 1852 | function frame:getObjectIndexTrace(objects, x, y) 1853 | local function sign(x1, y1, x2, y2, x3, y3) 1854 | return (x1 - x3) * (y2 - y3) - (x2 - x3) * (y1 - y3) 1855 | end 1856 | 1857 | local function isInTriangle(checkX, checkY, x1, y1, x2, y2, x3, y3, framex1, framey1, framex2, framey2) 1858 | local b1 = sign(checkX, checkY, x1, y1, x2, y2) < 0 1859 | local b2 = sign(checkX, checkY, x2, y2, x3, y3) < 0 1860 | local b3 = sign(checkX, checkY, x3, y3, x1, y1) < 0 1861 | 1862 | return b1 == b2 and b2 == b3 1863 | end 1864 | 1865 | local y = y - 1 1866 | 1867 | local selfX1 = self.x1 1868 | local selfY1 = self.y1 1869 | local selfX2 = self.x2 1870 | local selfY2 = self.y2 1871 | 1872 | x = x * 2 1873 | y = y * 3 + 1 1874 | 1875 | local camera = self.camera 1876 | 1877 | ---@class CameraAngles 1878 | local cameraAngles = { 1879 | sin(camera[4] or 0), cos(camera[4] or 0), 1880 | sin(-camera[5]), cos(-camera[5]), 1881 | sin(camera[6]), cos(camera[6]), 1882 | } 1883 | local cA1 = cameraAngles[1] 1884 | local cA2 = cameraAngles[2] 1885 | local cA3 = cameraAngles[3] 1886 | local cA4 = cameraAngles[4] 1887 | local cA5 = cameraAngles[5] 1888 | local cA6 = cameraAngles[6] 1889 | 1890 | local solutions = {} 1891 | for i = 1, #objects do 1892 | local object = objects[i] 1893 | 1894 | local model = object[7] 1895 | 1896 | local rotX = object[4] 1897 | local rotY = object[5] 1898 | local rotZ = object[6] 1899 | if (rotX and rotX ~= 0) or (rotY and rotY ~= 0) or (rotZ and rotZ ~= 0) then 1900 | model = rotateCollapsedModel(model, rotX, rotY, rotZ) 1901 | end 1902 | local oX = object[1] 1903 | local oY = object[2] 1904 | local oZ = object[3] 1905 | computePolyCamDistance(model, oX, oY, oZ, camera) 1906 | 1907 | local renderOffsetX = renderOffsetX 1908 | local renderOffsetY = renderOffsetY 1909 | 1910 | local sXFactor = sXFactor 1911 | local sYFactor = sYFactor 1912 | 1913 | local cA1 = cA1 1914 | local cA2 = cA2 1915 | local cA3 = cA3 1916 | local cA4 = cA4 1917 | local cA5 = cA5 1918 | local cA6 = cA6 1919 | 1920 | local xCameraOffset = oX - camera[1] 1921 | local yCameraOffset = oY - camera[2] 1922 | local zCameraOffset = oZ - camera[3] 1923 | 1924 | local function map3dTo2d(x, y, z) 1925 | local dX = x + xCameraOffset 1926 | local dY = y + yCameraOffset 1927 | local dZ = z + zCameraOffset 1928 | 1929 | local dX2 = cA4 * dX - cA3 * dZ 1930 | dZ = cA3 * dX + cA4 * dZ 1931 | dX = dX2 1932 | 1933 | local dY2 = cA6 * dY - cA5 * dX 1934 | dX = cA5 * dY + cA6 * dX 1935 | dY = dY2 1936 | 1937 | if cA1 ~= 0 then 1938 | local dZ2 = cA1 * dZ - cA2 * dY 1939 | dY = cA2 * dZ + cA1 * dY 1940 | dZ = dZ2 1941 | end 1942 | 1943 | local sX = (dZ / dX) * sXFactor + renderOffsetX 1944 | local sY = (dY / dX) * sYFactor + renderOffsetY 1945 | 1946 | return sX, sY, dX > 0 1947 | end 1948 | 1949 | for j = 1, #model do 1950 | local polygon = model[j] 1951 | 1952 | local x1, y1, onScreen1 = map3dTo2d(polygon[1], polygon[2], polygon[3]) 1953 | if onScreen1 then 1954 | local x2, y2, onScreen2 = map3dTo2d(polygon[4], polygon[5], polygon[6]) 1955 | if onScreen2 then 1956 | local x3, y3, onScreen3 = map3dTo2d(polygon[7], polygon[8], polygon[9]) 1957 | if onScreen3 then 1958 | if polygon[10] or (x2 - x1) * (y3 - y2) - (y2 - y1) * (x3 - x2) < 0 then 1959 | local avgX = xCameraOffset + (polygon[1] + polygon[4] + polygon[7]) / 3 1960 | local avgY = yCameraOffset + (polygon[2] + polygon[5] + polygon[8]) / 3 1961 | local avgZ = zCameraOffset + (polygon[3] + polygon[6] + polygon[9]) / 3 1962 | 1963 | local depth = avgX * avgX + avgY * avgY + avgZ * avgZ -- relative distance 1964 | 1965 | if isInTriangle(x, y, x1, y1, x2, y2, x3, y3, (selfX2 - 1) * 2 + 1, (selfY1 - 1) * 3 + 1, (selfX2) * 2, (selfY2 + 1) * 3) then 1966 | solutions[#solutions + 1] = {objectIndex = i, polygonIndex = j, depth = depth} 1967 | end 1968 | end 1969 | end 1970 | end 1971 | end 1972 | end 1973 | end 1974 | 1975 | if #solutions <= 0 then 1976 | return 1977 | elseif #solutions == 1 then 1978 | return solutions[1].objectIndex, solutions[1].polygonIndex, solutions[1].depth 1979 | end 1980 | 1981 | local closestSolution = -1 1982 | local closestSolutionDepth = math.huge 1983 | for i = 1, #solutions do 1984 | local solution = solutions[i] 1985 | if solution.depth < closestSolutionDepth then 1986 | closestSolutionDepth = solution.depth 1987 | closestSolution = i 1988 | end 1989 | end 1990 | 1991 | local solution = solutions[closestSolution] 1992 | return solution.objectIndex, solution.polygonIndex, solution.depth 1993 | end 1994 | 1995 | ---Creates a new PineObject 1996 | ---@param model string|Model|LoDModel either a string (path to the model file) or Model or LoDModel 1997 | ---@param x number? 1998 | ---@param y number? 1999 | ---@param z number? 2000 | ---@param rotX number? 2001 | ---@param rotY number? 2002 | ---@param rotZ number? 2003 | ---@return PineObject 2004 | function frame:newObject(model, x, y, z, rotX, rotY, rotZ) 2005 | local objModel = nil 2006 | local modelSize = nil 2007 | 2008 | if type(model) == "table" then 2009 | if model.initObject then 2010 | -- @cast model LoDModel 2011 | -- objModel, modelSize = loadModelRaw(model) 2012 | -- model:initObject(object) 2013 | else 2014 | ---@cast model Model 2015 | objModel, modelSize = loadModelRaw(model) 2016 | end 2017 | else 2018 | ---@cast model string 2019 | local modelRaw = loadModel(model) 2020 | objModel, modelSize = loadModelRaw(modelRaw) 2021 | end 2022 | 2023 | ---@class PineObject 2024 | local object = { 2025 | x, y, z, 2026 | rotX, rotY, rotZ, 2027 | objModel, modelSize, 2028 | } 2029 | object.frame = self 2030 | 2031 | ---Set the object's position 2032 | ---@param newX number? 2033 | ---@param newY number? 2034 | ---@param newZ number? 2035 | function object:setPos(newX, newY, newZ) 2036 | self[1] = newX or self[1] 2037 | self[2] = newY or self[2] 2038 | self[3] = newZ or self[3] 2039 | end 2040 | 2041 | ---Set the object's rotation 2042 | ---@param newRotX number? 2043 | ---@param newRotY number? 2044 | ---@param newRotZ number? 2045 | function object:setRot(newRotX, newRotY, newRotZ) 2046 | self[4] = newRotX or self[4] 2047 | self[5] = newRotY or self[5] 2048 | self[6] = newRotZ or self[6] 2049 | end 2050 | 2051 | ---Set the object's position 2052 | ---@param ref string|Model either a string (path to the model file) or Model 2053 | function object:setModel(ref) 2054 | if type(ref) == "table" then 2055 | ---@cast ref Model 2056 | objModel, modelSize = loadModelRaw(ref) 2057 | self[7] = objModel 2058 | self[8] = modelSize 2059 | else 2060 | ---@cast ref string 2061 | local modelRaw = loadModel(ref) 2062 | objModel, modelSize = loadModelRaw(modelRaw) 2063 | self[7] = objModel 2064 | self[8] = modelSize 2065 | end 2066 | end 2067 | 2068 | if type(model) == "table" then 2069 | if model.initObject then 2070 | ---@cast model LoDModel 2071 | model:initObject(object) 2072 | end 2073 | end 2074 | 2075 | return object 2076 | end 2077 | 2078 | frame:depthInterpolation(true) 2079 | 2080 | return frame 2081 | end 2082 | 2083 | local models = {} 2084 | local function newPoly(x1, y1, z1, x2, y2, z2, x3, y3, z3, c) 2085 | ---@class Polygon 2086 | ---@field x1 number 2087 | ---@field y1 number 2088 | ---@field z1 number 2089 | ---@field x2 number 2090 | ---@field y2 number 2091 | ---@field z2 number 2092 | ---@field x3 number 2093 | ---@field y3 number 2094 | ---@field z3 number 2095 | ---@field c number color 2096 | ---@field forceRender boolean? 2097 | 2098 | ---@type Polygon 2099 | local poly = { 2100 | x1 = x1, 2101 | y1 = y1, 2102 | z1 = z1, 2103 | x2 = x2, 2104 | y2 = y2, 2105 | z2 = z2, 2106 | x3 = x3, 2107 | y3 = y3, 2108 | z3 = z3, 2109 | c = c, 2110 | } 2111 | return poly 2112 | end 2113 | 2114 | ---@param options {color: number?, top: number?, side: number?, side2: number?, bottom: number?, bottom2: number?} 2115 | function models:cube(options) 2116 | options.color = options.color or colors.red 2117 | ---@class Model 2118 | local cube = { 2119 | newPoly(-.5, -.5, -.5, .5, -.5, .5, -.5, -.5, .5, options.bottom or options.color), 2120 | newPoly(-.5, -.5, -.5, .5, -.5, -.5, .5, -.5, .5, options.bottom2 or options.bottom or options.color), 2121 | newPoly(-.5, .5, -.5, -.5, .5, .5, .5, .5, .5, options.top or options.color), 2122 | newPoly(-.5, .5, -.5, .5, .5, .5, .5, .5, -.5, options.top or options.color), 2123 | newPoly(-.5, -.5, -.5, -.5, -.5, .5, -.5, .5, -.5, options.side or options.color), 2124 | newPoly(-.5, -.5, .5, -.5, .5, .5, -.5, .5, -.5, options.side2 or options.side or options.color), 2125 | newPoly(.5, -.5, -.5, .5, .5, .5, .5, -.5, .5, options.side or options.color), 2126 | newPoly(.5, -.5, -.5, .5, .5, -.5, .5, .5, .5, options.side2 or options.side or options.color), 2127 | newPoly(-.5, -.5, -.5, .5, .5, -.5, .5, -.5, -.5, options.side or options.color), 2128 | newPoly(-.5, -.5, -.5, -.5, .5, -.5, .5, .5, -.5, options.side2 or options.side or options.color), 2129 | newPoly(-.5, -.5, .5, .5, -.5, .5, -.5, .5, .5, options.side or options.color), 2130 | newPoly(.5, -.5, .5, .5, .5, .5, -.5, .5, .5, options.side2 or options.side or options.color), 2131 | } 2132 | 2133 | for name, func in pairs(transforms) do 2134 | cube[name] = func 2135 | end 2136 | 2137 | return cube 2138 | end 2139 | 2140 | ---@param options {res: number?, color: number?, color2: number?, colors: number?, top: number?, bottom: number?} 2141 | function models:sphere(options) 2142 | options.res = options.res or 32 2143 | options.color = options.color or colors.red 2144 | ---@class Model 2145 | local model = {} 2146 | local prevPoints = {} 2147 | for i = 0, options.res do 2148 | local y = 0.5 * cos(i / options.res * pi) 2149 | local newPrevPoints = {} 2150 | for j = 0, options.res do 2151 | local radius = 0.5 * sqrt(1 - (y * 2) * (y * 2)) 2152 | local x = cos(j / options.res * pi * 2) * radius 2153 | local z = sin(j / options.res * pi * 2) * radius 2154 | local x2 = cos((j + 1) / options.res * pi * 2) * radius 2155 | local z2 = sin((j + 1) / options.res * pi * 2) * radius 2156 | if (prevPoints[j]) then 2157 | model[#model + 1] = { 2158 | x1 = prevPoints[(j + 1) % options.res].x, 2159 | y1 = prevPoints[(j + 1) % options.res].y, 2160 | z1 = prevPoints[(j + 1) % options.res].z, 2161 | x2 = x, 2162 | y2 = y, 2163 | z2 = z, 2164 | x3 = prevPoints[j].x, 2165 | y3 = prevPoints[j].y, 2166 | z3 = prevPoints[j].z, 2167 | c = options.color, 2168 | } 2169 | model[#model + 1] = { 2170 | x1 = x2, 2171 | y1 = y, 2172 | z1 = z2, 2173 | x2 = x, 2174 | y2 = y, 2175 | z2 = z, 2176 | x3 = prevPoints[(j + 1) % options.res].x, 2177 | y3 = prevPoints[(j + 1) % options.res].y, 2178 | z3 = prevPoints[(j + 1) % options.res].z, 2179 | c = options.color2 or options.color, 2180 | } 2181 | end 2182 | 2183 | newPrevPoints[j] = {x = x, y = y, z = z} 2184 | end 2185 | prevPoints = newPrevPoints 2186 | end 2187 | 2188 | if options.colors or options.top or options.bottom then 2189 | for i = 1, #model do 2190 | local poly = model[i] 2191 | local avgY = (poly.y1 + poly.y2 + poly.y3) / 3 2192 | if options.colors then 2193 | local index = floor((-avgY + 0.5) * (#options.colors) + 1) 2194 | poly.c = options.colors[index] or poly.c 2195 | else 2196 | if avgY >= 0 then 2197 | poly.c = options.top or poly.c 2198 | else 2199 | poly.c = options.bottom or poly.c 2200 | end 2201 | end 2202 | end 2203 | end 2204 | 2205 | for name, func in pairs(transforms) do 2206 | model[name] = func 2207 | end 2208 | 2209 | return model 2210 | end 2211 | 2212 | ---@param options {res: number?, color: number?, color2: number?, colors: number[]?, top: number?, bottom: number?, colorsFractal: boolean?} 2213 | function models:icosphere(options) 2214 | options.res = options.res or 1 2215 | 2216 | local phi = (1 + sqrt(5)) / 2 2217 | local v = { 2218 | {phi, 1, 0}, 2219 | {phi, -1, 0}, 2220 | {-phi, -1, 0}, 2221 | {-phi, 1, 0}, 2222 | {1, 0, phi}, 2223 | {-1, 0, phi}, 2224 | {-1, 0, -phi}, 2225 | {1, 0, -phi}, 2226 | {0, phi, 1}, 2227 | {0, phi, -1}, 2228 | {0, -phi, -1}, 2229 | {0, -phi, 1}, 2230 | } 2231 | 2232 | local function buildPoly(i1, i2, i3) 2233 | return newPoly(v[i1][1], v[i1][2], v[i1][3], v[i2][1], v[i2][2], v[i2][3], v[i3][1], v[i3][2], v[i3][3], 2234 | options.colors and 1 or options.color) 2235 | end 2236 | 2237 | ---@class Model 2238 | local model = { 2239 | buildPoly(11, 2, 12), 2240 | buildPoly(11, 8, 2), 2241 | buildPoly(11, 7, 8), 2242 | buildPoly(11, 3, 7), 2243 | buildPoly(11, 12, 3), 2244 | 2245 | buildPoly(4, 7, 3), 2246 | buildPoly(4, 10, 7), 2247 | buildPoly(4, 9, 10), 2248 | buildPoly(4, 6, 9), 2249 | buildPoly(4, 3, 6), 2250 | 2251 | buildPoly(5, 6, 12), 2252 | buildPoly(5, 9, 6), 2253 | buildPoly(5, 1, 9), 2254 | buildPoly(5, 2, 1), 2255 | buildPoly(5, 12, 2), 2256 | 2257 | buildPoly(3, 12, 6), 2258 | buildPoly(1, 8, 10), 2259 | buildPoly(1, 10, 9), 2260 | buildPoly(1, 2, 8), 2261 | buildPoly(10, 8, 7), 2262 | } 2263 | 2264 | local function subdivide() 2265 | local newModel = {} 2266 | for i = 1, #model do 2267 | local poly = model[i] 2268 | 2269 | local AB = { 2270 | x = (poly.x1 + poly.x2) / 2, 2271 | y = (poly.y1 + poly.y2) / 2, 2272 | z = (poly.z1 + poly.z2) / 2, 2273 | } 2274 | local AC = { 2275 | x = (poly.x1 + poly.x3) / 2, 2276 | y = (poly.y1 + poly.y3) / 2, 2277 | z = (poly.z1 + poly.z3) / 2, 2278 | } 2279 | local BC = { 2280 | x = (poly.x2 + poly.x3) / 2, 2281 | y = (poly.y2 + poly.y3) / 2, 2282 | z = (poly.z2 + poly.z3) / 2, 2283 | } 2284 | 2285 | local nextColor = poly.c 2286 | if options.colorsFractal then 2287 | nextColor = (nextColor % #options.colors) + 1 2288 | end 2289 | 2290 | newModel[#newModel + 1] = newPoly(AB.x, AB.y, AB.z, BC.x, BC.y, BC.z, AC.x, AC.y, AC.z, poly.c) 2291 | newModel[#newModel + 1] = newPoly(poly.x1, poly.y1, poly.z1, AB.x, AB.y, AB.z, AC.x, AC.y, AC.z, nextColor) 2292 | newModel[#newModel + 1] = newPoly(AB.x, AB.y, AB.z, poly.x2, poly.y2, poly.z2, BC.x, BC.y, BC.z, nextColor) 2293 | newModel[#newModel + 1] = newPoly(AC.x, AC.y, AC.z, BC.x, BC.y, BC.z, poly.x3, poly.y3, poly.z3, nextColor) 2294 | end 2295 | model = newModel 2296 | end 2297 | 2298 | for i = 1, options.res - 1 do 2299 | subdivide() 2300 | end 2301 | 2302 | local function forceLength(x, y, z) 2303 | local length = math.sqrt(x * x + y * y + z * z) 2304 | local ratio = 0.5 / length 2305 | return x * ratio, y * ratio, z * ratio 2306 | end 2307 | 2308 | for i = 1, #model do 2309 | local poly = model[i] 2310 | poly.x1, poly.y1, poly.z1 = forceLength(poly.x1, poly.y1, poly.z1) 2311 | poly.x2, poly.y2, poly.z2 = forceLength(poly.x2, poly.y2, poly.z2) 2312 | poly.x3, poly.y3, poly.z3 = forceLength(poly.x3, poly.y3, poly.z3) 2313 | if not options.colorsFractal then 2314 | local avgY = (poly.y1 + poly.y2 + poly.y3) / 3 2315 | if (options.colors) then 2316 | local index = math.floor((-avgY + 0.5) * (#options.colors) + 1) 2317 | poly.c = options.colors[index] or poly.c 2318 | else 2319 | if (avgY >= 0) then 2320 | poly.c = options.top or poly.c 2321 | else 2322 | poly.c = options.bottom or poly.c 2323 | end 2324 | end 2325 | else 2326 | poly.c = options.colors[poly.c] 2327 | end 2328 | end 2329 | 2330 | for name, func in pairs(transforms) do 2331 | model[name] = func 2332 | end 2333 | 2334 | return model 2335 | end 2336 | 2337 | ---@param options {size: number?, color: number?, y: number?} 2338 | function models:plane(options) 2339 | options.color = options.color or colors.lime 2340 | options.size = options.size or 1 2341 | options.y = options.y or 0 2342 | ---@class Model 2343 | local plane = { 2344 | newPoly(-1 * options.size, options.y, 1 * options.size, 1 * options.size, options.y, -1 * options.size, -1 * options.size, options.y, 2345 | -1 * options.size, options.color), 2346 | newPoly(-1 * options.size, options.y, 1 * options.size, 1 * options.size, options.y, 1 * options.size, 1 * options.size, options.y, 2347 | -1 * options.size, options.color), 2348 | } 2349 | 2350 | for name, func in pairs(transforms) do 2351 | plane[name] = func 2352 | end 2353 | 2354 | return plane 2355 | end 2356 | 2357 | ---@param options {res: number?, randomOffset: number?, height: number?, randomHeight: number?, y: number?, scale: number?, color: number?, snowColor: number?, snow: boolean?, snowHeight: number?} 2358 | function models:mountains(options) 2359 | options.res = options.res or 20 2360 | options.randomOffset = options.randomOffset or 0 2361 | options.height = options.height or 1 2362 | options.randomHeight = options.randomHeight or 0 2363 | options.y = options.y or 0 2364 | options.scale = options.scale or 100 2365 | options.color = options.color or colors.green 2366 | options.snowColor = options.snowColor or colors.white 2367 | 2368 | local minHeight = 3 / options.res * options.height / (options.randomHeight + 1) 2369 | local maxHeight = 3 / options.res * options.height * (options.randomHeight + 1) 2370 | 2371 | ---@class Model 2372 | local model = {} 2373 | for i = 0, options.res do 2374 | local offset = math.random(-options.randomOffset * 100, options.randomOffset * 100) / 100 2375 | local pos = i + offset 2376 | local x1 = cos((pos - 1) / options.res * pi * 2) * options.scale 2377 | local z1 = sin((pos - 1) / options.res * pi * 2) * options.scale 2378 | local x2 = cos((pos - 0.5) / options.res * pi * 2) * options.scale 2379 | local z2 = sin((pos - 0.5) / options.res * pi * 2) * options.scale 2380 | local x3 = cos(pos / options.res * pi * 2) * options.scale 2381 | local z3 = sin(pos / options.res * pi * 2) * options.scale 2382 | 2383 | local mountainHeight = math.random(minHeight * 100, maxHeight * 100) / 100 * options.scale 2384 | 2385 | local polygon = { 2386 | x1 = x1, 2387 | y1 = options.y, 2388 | z1 = z1, 2389 | x2 = x3, 2390 | y2 = options.y, 2391 | z2 = z3, 2392 | x3 = x2, 2393 | y3 = options.y + mountainHeight, 2394 | z3 = z2, 2395 | c = options.color, 2396 | forceRender = true, 2397 | } 2398 | model[#model + 1] = polygon 2399 | 2400 | if options.snow then 2401 | local snowDistance = 0.93 2402 | local realSnowRatio = options.snowHeight or 0.5 2403 | local snowRatio = 1 - (realSnowRatio * maxHeight) / (mountainHeight / options.scale) 2404 | snowRatio = max(0, min(1, snowRatio)) 2405 | 2406 | if snowRatio > 0.2 then 2407 | local snowPolygon = { 2408 | x1 = (x1 * snowRatio + x2 * (1 - snowRatio)) * snowDistance, 2409 | y1 = options.y + mountainHeight * (1 - snowRatio), 2410 | z1 = (z1 * snowRatio + z2 * (1 - snowRatio)) * snowDistance, 2411 | x2 = (x3 * snowRatio + x2 * (1 - snowRatio)) * snowDistance, 2412 | y2 = options.y + mountainHeight * (1 - snowRatio), 2413 | z2 = (z3 * snowRatio + z2 * (1 - snowRatio)) * snowDistance, 2414 | x3 = x2 * snowDistance, 2415 | y3 = options.y + mountainHeight, 2416 | z3 = z2 * snowDistance, 2417 | c = options.snowColor, 2418 | forceRender = true, 2419 | } 2420 | model[#model + 1] = snowPolygon 2421 | end 2422 | end 2423 | end 2424 | 2425 | for name, func in pairs(transforms) do 2426 | model[name] = func 2427 | end 2428 | 2429 | return model 2430 | end 2431 | 2432 | return { 2433 | newFrame = newFrame, 2434 | loadModel = loadModel, 2435 | newBuffer = newBuffer, 2436 | models = models, 2437 | transforms = transforms, 2438 | } 2439 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pine3D 2 | Most efficient 3d graphics library for ComputerCraft! 3 | 4 | [hp_mountains.webm](https://user-images.githubusercontent.com/36447113/206931538-dcc76d79-2e06-4b53-bc00-b91f1843bcd7.webm) 5 | 6 | ## Documentation 7 | All documentation can be found on https://pine3d.cc/ 8 | 9 | ## Help / suggestions / show your creations 10 | On our Discord server: https://discord.gg/MjsNjK2psB 11 | -------------------------------------------------------------------------------- /betterblittle.lua: -------------------------------------------------------------------------------- 1 | -- Made by Xella 2 | local floor = math.floor 3 | local min = math.min 4 | local concat = table.concat 5 | 6 | local colorMap = {} 7 | for i = 1, 16 do colorMap[2 ^ (i - 1)] = i end 8 | 9 | local colorChar = {} 10 | for i = 1, 16 do colorChar[i] = ("0123456789abcdef"):sub(i, i) end 11 | 12 | local colorDistances 13 | 14 | local function getColorsFromPixelGroup(p1, p2, p3, p4, p5, p6) 15 | local freq = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} 16 | freq[p1] = 1 17 | freq[p2] = freq[p2] + 1 18 | freq[p3] = freq[p3] + 1 19 | freq[p4] = freq[p4] + 1 20 | freq[p5] = freq[p5] + 1 21 | freq[p6] = freq[p6] + 1 22 | 23 | local c1 = p1 24 | local c2 = p1 25 | local totalColors = 0 26 | local highestCount = 0 27 | for color = 1, 16 do 28 | local count = freq[color] 29 | if count > 0 then 30 | totalColors = totalColors + 1 31 | if color ~= c1 then c2 = color end 32 | if count > highestCount then 33 | c2 = c1 34 | c1 = color 35 | highestCount = count 36 | end 37 | end 38 | end 39 | 40 | if totalColors <= 2 then return c1, c2 end 41 | 42 | local bestC2 = p1 43 | local lowestError = 99 44 | local c1Dists = colorDistances[c1] 45 | for c2 = 1, 16 do 46 | if c2 ~= c1 and freq[c2] > 0 then 47 | local c2Dists = colorDistances[c2] 48 | local err = min(c1Dists[p1], c2Dists[p1]) + min(c1Dists[p2], c2Dists[p2]) + min(c1Dists[p3], c2Dists[p3]) 49 | + min(c1Dists[p4], c2Dists[p4]) + min(c1Dists[p5], c2Dists[p5]) + min(c1Dists[p6], c2Dists[p6]) 50 | if err < lowestError then 51 | lowestError = err 52 | bestC2 = c2 53 | end 54 | end 55 | end 56 | 57 | return c1, bestC2 58 | end 59 | 60 | -- Based on https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab 61 | local function sRGBtoOklab(r, g, b) 62 | local function f(v) return v >= 0.04045 and ((v + 0.055) / (1 + 0.055)) ^ 2.4 or (v / 12.92) end 63 | r, g, b = f(r), f(g), f(b) 64 | local l = (0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b) ^ (1 / 3) 65 | local m = (0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b) ^ (1 / 3) 66 | local s = (0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b) ^ (1 / 3) 67 | return 68 | 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s, 69 | 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s, 70 | 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s 71 | end 72 | 73 | ---Compute distances between colors from current palette using a color space 74 | ---@param window Redirect 75 | ---@param colorSpace "Oklab" | "sRGB" | nil default: Oklab 76 | local function computeColorDistances(window, colorSpace) 77 | colorDistances = {} 78 | for c1 = 1, 16 do 79 | local r1, g1, b1 = window.getPaletteColor(2 ^ (c1 - 1)) 80 | if colorSpace ~= "sRGB" then r1, g1, b1 = sRGBtoOklab(r1, g1, b1) end 81 | local distances = {} 82 | for c2 = 1, 16 do 83 | local r2, g2, b2 = window.getPaletteColor(2 ^ (c2 - 1)) 84 | if colorSpace ~= "sRGB" then r2, g2, b2 = sRGBtoOklab(r2, g2, b2) end 85 | distances[c2] = (r2 - r1) ^ 2 + (g2 - g1) ^ 2 + (b2 - b1) ^ 2 86 | end 87 | colorDistances[c1] = distances 88 | end 89 | end 90 | 91 | local char = string.char 92 | local allChars = {} 93 | for i = 1, 32 do allChars[i] = char(i + 127) end 94 | local function getCharFomPixelGroup(p1, p2, p3, p4, p5, p6) 95 | local c1, c2 = getColorsFromPixelGroup(p1, p2, p3, p4, p5, p6) 96 | local c1Dist = colorDistances[c1] 97 | local c2Dist = colorDistances[c2] 98 | if c1Dist[p6] < c2Dist[p6] then 99 | local charNr = (c1Dist[p1] < c2Dist[p1] and 31 or 32) 100 | - (c1Dist[p2] < c2Dist[p2] and 2 or 0) 101 | - (c1Dist[p3] < c2Dist[p3] and 4 or 0) 102 | - (c1Dist[p4] < c2Dist[p4] and 8 or 0) 103 | - (c1Dist[p5] < c2Dist[p5] and 16 or 0) 104 | return allChars[charNr], c2, c1 105 | else 106 | local charNr = (c1Dist[p1] < c2Dist[p1] and 2 or 1) 107 | + (c1Dist[p2] < c2Dist[p2] and 2 or 0) 108 | + (c1Dist[p3] < c2Dist[p3] and 4 or 0) 109 | + (c1Dist[p4] < c2Dist[p4] and 8 or 0) 110 | + (c1Dist[p5] < c2Dist[p5] and 16 or 0) 111 | return allChars[charNr], c1, c2 112 | end 113 | end 114 | 115 | ---Draw a color buffer to the window 116 | ---@param buffer integer[][] 2D array of colors to display 117 | ---@param window Redirect 118 | ---@param wx integer? x position on monitor (in terminal character pixels) 119 | ---@param wy integer? y position on monitor (in terminal character pixels) 120 | local function drawBuffer(buffer, window, wx, wy) 121 | wx = wx or 1 122 | wy = wy or 1 123 | 124 | local height = #buffer 125 | local width = #buffer[1] 126 | 127 | if not colorDistances then computeColorDistances(window) end 128 | 129 | local maxX = floor(width / 2) 130 | local setCursorPos = window.setCursorPos 131 | local blit = window.blit 132 | local colorChar = colorChar 133 | for y = 0, floor(height / 3) - 1 do 134 | local oy = y * 3 + 1 135 | 136 | local r1 = buffer[oy] -- first row from buffer for this row of characters 137 | local r2 = buffer[oy + 1] -- second row from buffer for this row of characters 138 | local r3 = buffer[oy + 2] -- third row from buffer for this row of characters 139 | 140 | local blitC1 = {nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil} 141 | local blitC2 = {nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil} 142 | local blitChar = {nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil} 143 | for x = 1, maxX do 144 | local ox = (x - 1) * 2 + 1 145 | 146 | local p1 = colorMap[r1[ox]] 147 | local p2 = colorMap[r1[ox + 1]] 148 | local p3 = colorMap[r2[ox]] 149 | local p4 = colorMap[r2[ox + 1]] 150 | local p5 = colorMap[r3[ox]] 151 | local p6 = colorMap[r3[ox + 1]] 152 | if p1 == p2 and p2 == p3 and p3 == p4 and p4 == p5 and p5 == p6 then 153 | local c = colorChar[p1] 154 | blitC1[x] = c 155 | blitC2[x] = c 156 | blitChar[x] = "\x80" 157 | else 158 | local char, c1, c2 = getCharFomPixelGroup(p1, p2, p3, p4, p5, p6) 159 | blitC1[x] = colorChar[c1] 160 | blitC2[x] = colorChar[c2] 161 | blitChar[x] = char 162 | end 163 | end 164 | local con = concat 165 | local c1 = con(blitChar) 166 | local c2 = con(blitC1) 167 | local c3 = con(blitC2) 168 | setCursorPos(wx, wy + y) 169 | blit(c1, c2, c3) 170 | end 171 | end 172 | 173 | return { 174 | drawBuffer = drawBuffer, 175 | recomputeColorDistances = computeColorDistances 176 | } 177 | -------------------------------------------------------------------------------- /converter/bitmap.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | This single-file Lua library implements read/write support for the 3 | `Windows Bitmap`/`device-independent bitmap` file format version 3.0. 4 | 5 | Compatible with Lua5.1, LuaJIT, Lua5.2, Lua5.3, Lua5.4. 6 | 7 | License: MIT (see included file LICENSE) 8 | ]] 9 | 10 | -- offsets into a Windows Bitmap file header 11 | local bmp_header_offset = 0 12 | local bmp_header_filesize = 2 13 | local bmp_header_pixel_offset = 10 14 | local bmp_header_size = 14 15 | local bmp_header_width = 18 16 | local bmp_header_height = 22 17 | local bmp_header_planes = 26 18 | local bmp_header_bpp = 28 19 | local bmp_header_compression = 30 20 | local bmp_header_image_size = 34 21 | 22 | local function get_data_size(width, height, bpp) 23 | local line_w = math.ceil(width/4)*4 24 | return height*line_w*(bpp/8) 25 | end 26 | 27 | local function new_bitmap() 28 | -- this table is returned as an interface for a Windows Bitmap to the user: 29 | local bmp = {} 30 | 31 | -- reading 8/16/32-bit little-endian integer values from a string 32 | function bmp:read(offset) -- read uint8 33 | offset = math.floor(assert(tonumber(offset))) 34 | local value = assert(self.data[offset]):byte() 35 | return value 36 | end 37 | function bmp:read_word(offset) -- read uint16 38 | return self:read(offset+1)*0x100 + self:read(offset) 39 | end 40 | function bmp:read_dword(offset) -- read uint32 41 | return self:read(offset+3)*0x1000000 + self:read(offset+2)*0x10000 + self:read(offset+1)*0x100 + self:read(offset) 42 | end 43 | function bmp:read_long(offset) -- read int32 44 | local value = self:read_dword(offset) 45 | if value >= 0x80000000 then -- bitmap format uses two's complement 46 | value = -(value - 0x80000000) 47 | end 48 | return value 49 | end 50 | 51 | -- writing 8/16/32-bit little-endian integer values from a string 52 | function bmp:write(offset, data) -- write uint8* 53 | offset = math.floor(assert(tonumber(offset))) 54 | for i=1, #data do 55 | local index = offset+i-1 56 | local value = data:sub(i,i) 57 | assert(self.data[index]) -- only update, don't create new entry 58 | self.data[index] = value 59 | end 60 | end 61 | function bmp:write_word(offset, value) -- write uint16 62 | local a = math.floor(value % 0x100) 63 | local b = math.floor(value / 0x100) 64 | local data = string.char(a,b) 65 | self:write(offset, data) 66 | end 67 | function bmp:write_dword(offset, value) -- write uint32 68 | local a = math.floor(value) % 0x100 69 | local b = math.floor(value / 0x100) % 0x100 70 | local c = math.floor(value / 0x10000) % 0x100 71 | local d = math.floor(value / 0x1000000) % 0x100 72 | local data = string.char(a,b,c,d) 73 | self:write(offset, data) 74 | end 75 | function bmp:write_long(offset, value) 76 | if value < 0 then 77 | value = -value + 0x80000000 78 | end 79 | self:write_dword(offset, value) 80 | end 81 | 82 | -- read bitmap headers and parse required metadata, update self 83 | function bmp:read_header() 84 | -- check the bitmap header 85 | if not self:read_word(bmp_header_offset) == 0x4D42 then 86 | return nil, "Bitmap magic header not found" 87 | end 88 | local compression = self:read_dword(bmp_header_compression) 89 | if compression ~= 0 then 90 | return nil, "Only uncompressed bitmaps supported. Is: "..tostring(compression) 91 | end 92 | 93 | -- get bits per pixel from the bitmap header 94 | -- this library only supports 24bpp and 32bpp pixel formats! 95 | self.bpp = self:read_word(bmp_header_bpp) 96 | if not ((self.bpp == 24) or (self.bpp == 32)) then 97 | return nil, "Only 24bpp/32bpp bitmaps supported. Is: "..tostring(self.bpp) 98 | end 99 | 100 | -- get other required info from the bitmap header 101 | self.pixel_offset = self:read_dword(bmp_header_pixel_offset) 102 | self.width = self:read_long(bmp_header_width) 103 | self.height = self:read_long(bmp_header_height) 104 | 105 | -- calculate expected size of the data region 106 | self.data_size = get_data_size(self.width, self.height, self.bpp) 107 | 108 | -- if height is <0, the image data is in topdown format 109 | self.topdown = true 110 | if self.height < 0 then 111 | self.topdown = false 112 | self.height = -self.height 113 | end 114 | 115 | return true 116 | end 117 | 118 | -- write bitmap headers from self 119 | function bmp:write_header(width, height, bpp) 120 | if (width < 0) or (height < 0) or (width >= 2^31) or (height >= 2^31) then 121 | return nil, "Invalid dimensions" 122 | end 123 | if not ((bpp == 24) or (bpp == 32)) then 124 | return nil, "Invalid bpp" 125 | end 126 | 127 | -- update expected data size 128 | self.data_size = get_data_size(width, height, bpp) 129 | 130 | -- Bitmap header 131 | self:write_word(bmp_header_offset, 0x4D42) 132 | 133 | self:write_dword(bmp_header_filesize, 54+self.data_size) 134 | self:write_dword(bmp_header_size, 40) 135 | self:write_word(bmp_header_planes, 1) 136 | 137 | -- image information 138 | self:write_dword(bmp_header_compression, 0) 139 | self:write_word(bmp_header_bpp, bpp) 140 | self:write_dword(bmp_header_pixel_offset, self.pixel_offset) 141 | 142 | local line_w = math.ceil(width/4)*4 143 | self:write_long(bmp_header_width, line_w) 144 | --self:write_long(bmp_header_width, width) 145 | self:write_dword(bmp_header_image_size, self.data_size) 146 | 147 | if self.topdown then 148 | self:write_long(bmp_header_height, height) 149 | else 150 | self:write_long(bmp_header_height, -height) 151 | end 152 | 153 | -- set all internal values accordingly 154 | self:read_header() 155 | 156 | return true 157 | end 158 | 159 | -- return the r,g,b,a[0-255] color value for a pixel by its x,y coordinates 160 | function bmp:get_pixel(x,y) 161 | if (x < 0) or (x >= self.width) or (y < 0) or (y >= self.height) then 162 | return nil, "Out of bounds" 163 | end 164 | 165 | -- calculate byte offset in data 166 | local Bpp = self.bpp/8 167 | local line_w = math.ceil(self.width/4)*4 168 | local index = self.pixel_offset + y*line_w*Bpp + x*Bpp 169 | if self.topdown then 170 | index = self.pixel_offset + (self.height-y-1)*Bpp*line_w + x*Bpp 171 | end 172 | 173 | -- read r,g,b color values 174 | local b = self:read(index) 175 | local g = self:read(index+1) 176 | local r = self:read(index+2) 177 | 178 | 179 | local a = nil 180 | if Bpp == 4 then -- on 32bpp, also get 4th channel value(alpha, not in spec) 181 | a = self:read(index+3) 182 | end 183 | 184 | return r,g,b,a 185 | end 186 | 187 | -- set the color value at x,y 188 | function bmp:set_pixel(x,y, r,g,b,a) 189 | if (x < 0) or (x >= self.width) or (y < 0) or (y >= self.height) then 190 | return nil, "out of bounds" 191 | end 192 | if (r < 0) or (r > 255) or (g < 0) or (g > 255) or (b < 0) or (b > 255) then 193 | return nil, "invalid color" 194 | end 195 | if a and ((a < 0) or (a > 255)) then 196 | return nil, "invalid alpha" 197 | end 198 | 199 | -- calculate byte offset in data 200 | local Bpp = self.bpp/8 201 | local line_w = math.ceil(self.width/4)*4 202 | local index = self.pixel_offset + y*line_w*Bpp + x*Bpp 203 | if self.topdown then 204 | index = self.pixel_offset + (self.height-y-1)*Bpp*line_w + x*Bpp 205 | end 206 | 207 | -- write new pixel value 208 | if Bpp == 3 then 209 | self:write(index, string.char(b,g,r)) 210 | else 211 | self:write(index, string.char(b,g,r,a or 255)) 212 | end 213 | 214 | return true 215 | end 216 | 217 | -- return the entire bitmap serialized 218 | function bmp:tostring() 219 | return self.data[0]..table.concat(self.data) 220 | end 221 | 222 | return bmp 223 | end 224 | 225 | -- Create a new empty bitmap 226 | local function new_empty_bitmap(width, height, alpha) 227 | local bmp = new_bitmap() 228 | local bpp = 24 229 | if alpha then 230 | bpp = 32 231 | end 232 | 233 | -- create empty data string 234 | bmp.data = {} 235 | for i=1, 54+get_data_size(width, height, bpp) do 236 | bmp.data[i-1] = "\000" 237 | end 238 | bmp.pixel_offset = 54 239 | bmp.topdown = true 240 | 241 | -- write a new header to the data 242 | local ok,err = bmp:write_header(width, height, bpp) 243 | if not ok then 244 | return nil, err 245 | end 246 | 247 | return bmp 248 | end 249 | 250 | -- Read a bitmap from a string and return it 251 | local function new_bitmap_from_string(data) 252 | local bmp = new_bitmap() 253 | 254 | -- copy data from string to internal table 255 | bmp.data = {} 256 | for i=1, #data do 257 | bmp.data[i-1] = data:sub(i,i) 258 | end 259 | 260 | -- read the header from the data 261 | local ok,err = bmp:read_header() 262 | if not ok then 263 | return nil, err 264 | end 265 | 266 | return bmp 267 | end 268 | 269 | -- Read a bitmap from a file and return it 270 | local function new_bitmap_from_file(path) 271 | -- open a file containing bitmap data 272 | local file = io.open(path, "rb") 273 | if not file then 274 | return nil, "can't open input file for reading: "..tostring(path) 275 | end 276 | 277 | -- try to read from the file 278 | local data = file:read("*a") 279 | if (not data) or (data == "") then 280 | return nil, "can't read input file: "..tostring(path) 281 | end 282 | 283 | return new_bitmap_from_string(data) 284 | end 285 | 286 | 287 | -- this is the module returned to the user when require()'d 288 | local Bitmap = { 289 | empty_bitmap = new_empty_bitmap, 290 | from_string = new_bitmap_from_string, 291 | from_file = new_bitmap_from_file, 292 | _new_bitmap = new_bitmap 293 | } 294 | 295 | return Bitmap 296 | -------------------------------------------------------------------------------- /converter/bmpConverter.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Made by Xella#8655 3 | 4 | local bitmap = require("bitmap") 5 | 6 | term.write("> filename: ") 7 | local path = shell.dir() .. "/" .. read() 8 | 9 | print("Loading " .. path .. ".bmp") 10 | local file = fs.open(path .. ".bmp", "rb") 11 | 12 | if not file then 13 | error("Failed to load file from path " .. path .. ".bmp") 14 | end 15 | 16 | local raw = file:readAll() 17 | file:close() 18 | 19 | local bmp = bitmap.from_string(raw) 20 | 21 | if not bmp then 22 | error("Failed to parse the bmp!") 23 | end 24 | 25 | local termColors = {} 26 | for i = 1, 16 do 27 | local color = 2 ^ (i - 1) 28 | local r, g, b = term.getPaletteColor(color) 29 | local char = ("0123456789abcdef"):sub(i, i) 30 | termColors[#termColors+1] = {r=r, g=g, b=b, code=char, color=color} 31 | end 32 | 33 | local huge = math.huge 34 | local abs = math.abs 35 | local function closestCCColor(r, g, b) 36 | local closest = termColors[1] 37 | if not r or not g or not b then 38 | return closest 39 | end 40 | 41 | local closestDistance = huge 42 | 43 | for _, termColor in pairs(termColors) do 44 | local distance = abs(r/255 - termColor.r) + abs(g/255 - termColor.g) + abs(b/255 - termColor.b) 45 | if (distance < closestDistance) then 46 | closest = termColor 47 | closestDistance = distance 48 | end 49 | end 50 | 51 | return closest 52 | end 53 | 54 | local paintutilsImg = "" 55 | for y = 1, bmp.height do 56 | local row = {} 57 | for x = 1, bmp.width do 58 | local r, g, b = bmp:get_pixel(x, y) 59 | local closest = closestCCColor(r, g, b) 60 | row[x] = closest.code 61 | end 62 | paintutilsImg = paintutilsImg .. table.concat(row) .. "\n" 63 | end 64 | 65 | print("Got " .. (#paintutilsImg) .. " px") 66 | 67 | local outFile = fs.open(path .. ".nfp", "w") 68 | outFile.write(paintutilsImg) 69 | outFile:close() 70 | 71 | print("Saved to " .. path .. ".nfp !") 72 | -------------------------------------------------------------------------------- /converter/objConverter.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Made by Xella#8655 3 | 4 | local objLoader = require("objLoader") 5 | 6 | term.write("> filename: ") 7 | local filename = read() 8 | 9 | local model = objLoader.load(filename) 10 | 11 | print("Serializing model...") 12 | if #model > 5000 then 13 | term.setTextColor(colors.orange) 14 | print("Model has " .. (#model) .. " polygons. This can take a while...") 15 | term.setTextColor(colors.white) 16 | end 17 | model = textutils.serialise(model) 18 | 19 | print("Saving model...") 20 | local file = fs.open("models/" .. filename, "w") -- Make sure you save the model in the models folder with a new name 21 | file.write(model) 22 | file.close() 23 | 24 | term.setTextColor(colors.lime) 25 | print("Done!") 26 | term.setTextColor(colors.white) 27 | -------------------------------------------------------------------------------- /converter/objLoader.lua: -------------------------------------------------------------------------------- 1 | 2 | local path = "/objModels" 3 | 4 | local termColors = {} 5 | for i = 1, 16 do 6 | local color = 2 ^ (i - 1) 7 | local r, g, b = term.getPaletteColor(color) 8 | local char = ("0123456789abcdef"):sub(i, i) 9 | termColors[#termColors+1] = {r=r, g=g, b=b, code=char, color=color} 10 | end 11 | 12 | local abs = math.abs 13 | local function closestCCColor(r, g, b) 14 | local closest = termColors[1] 15 | if not r or not g or not b then 16 | return closest 17 | end 18 | 19 | local closestDistance = math.huge 20 | 21 | for i, termColor in pairs(termColors) do 22 | local distance = abs(r - termColor.r) + abs(g - termColor.g) + abs(b - termColor.b) 23 | if (distance < closestDistance) then 24 | local color = 2 ^ (i - 1) 25 | closest = color 26 | closestDistance = distance 27 | end 28 | end 29 | 30 | return closest 31 | end 32 | 33 | local shrink = 1 34 | local function loadTexture(filename) 35 | print("Loading texture " .. filename) 36 | 37 | local textureFile = fs.open(path .. "/" .. filename, "r") 38 | local raw = textureFile:readAll() 39 | textureFile:close() 40 | 41 | local texture = {} 42 | local lineNr = 0 43 | for line in raw:gmatch("[^\n]+") do 44 | if lineNr % shrink == 0 then 45 | local row = {} 46 | local columnNr = 0 47 | for char in line:gmatch(".") do 48 | if columnNr % shrink == 0 then 49 | row[#row+1] = char 50 | end 51 | columnNr = columnNr + 1 52 | end 53 | texture[#texture+1] = row 54 | end 55 | lineNr = lineNr + 1 56 | end 57 | 58 | print("Finished loading texture (" .. #texture[1] .. " x " .. #texture .. " px)") 59 | 60 | return texture 61 | end 62 | 63 | function loadMTLFile(dir, filename) 64 | local mtlFile = fs.open(path .. "/" .. dir .. "/" .. filename, "r") 65 | local raw = mtlFile:readAll() 66 | mtlFile:close() 67 | 68 | local materialList = {} 69 | local materialMap = {} 70 | 71 | local material = {} 72 | for line in raw:gmatch("[^\n]+") do 73 | local parts = {} 74 | for part in line:gmatch("[^%s]+") do 75 | parts[#parts+1] = part 76 | end 77 | 78 | if parts[1] == "newmtl" then 79 | material = {} 80 | 81 | materialList[#materialList+1] = material 82 | materialMap[parts[2]] = material 83 | elseif parts[1] == "map_Kd" then 84 | local filename = parts[2]:sub(1, parts[2]:find("%.")-1) .. ".nfp" 85 | material.texture = loadTexture(dir .. "/" .. filename) 86 | elseif parts[1] == "Kd" then 87 | local r = tonumber(parts[2]) 88 | local g = tonumber(parts[3]) 89 | local b = tonumber(parts[4]) 90 | 91 | local function adjusted(c) 92 | local srgb = 0 93 | if c < 0.0031308 then 94 | if c < 0.0 then 95 | srgb = 0.0 96 | else 97 | srgb = c * 12.92 98 | end 99 | else 100 | srgb = 1.055 * math.pow(c, 1.0 / 2.4) - 0.055 101 | end 102 | 103 | return math.max(math.min(math.floor(srgb * 255 + 0.5), 255), 0) 104 | end 105 | 106 | r = adjusted(r)/255 107 | g = adjusted(g)/255 108 | b = adjusted(b)/255 109 | 110 | local color = closestCCColor(r, g, b) 111 | material.baseColor = color 112 | elseif not parts[1]:sub(1, 1) == "#" then 113 | if #parts == 2 then 114 | material[parts[1]] = parts[2] 115 | elseif #parts == 3 then 116 | material[parts[1]] = {parts[2], parts[3]} 117 | elseif #parts == 4 then 118 | material[parts[1]] = {parts[2], parts[3], parts[4]} 119 | end 120 | end 121 | end 122 | 123 | return { 124 | list = materialList, 125 | map = materialMap, 126 | } 127 | end 128 | 129 | function loadObjFile(id) 130 | local path = path .. "/" .. id .. "/" .. id .. ".obj" 131 | print("Opening " .. path) 132 | local ShrekFile = fs.open(path, "r") 133 | 134 | if not ShrekFile then 135 | error("Failed to load obj file from path " .. path) 136 | end 137 | 138 | local raw = ShrekFile:readAll() 139 | ShrekFile:close() 140 | 141 | local colorChar = {} 142 | for i = 1, 16 do 143 | local color = 2 ^ (i - 1) 144 | local char = ("0123456789abcdef"):sub(i, i) 145 | colorChar[char] = color 146 | end 147 | 148 | local materialsWarned = {} 149 | local quadsWarned = false 150 | 151 | local vertices = {} 152 | local vts = {} 153 | local material = "default" 154 | local materials = {} 155 | 156 | local convertedModel = {} 157 | 158 | for line in raw:gmatch("[^\n]+") do 159 | local parts = {} 160 | for part in line:gmatch("[^%s]+") do 161 | parts[#parts+1] = part 162 | end 163 | 164 | if parts[1] == "v" then 165 | vertices[#vertices+1] = { 166 | tonumber(parts[2]), 167 | tonumber(parts[3]), 168 | tonumber(parts[4]), 169 | } 170 | elseif parts[1] == "vt" then 171 | vts[#vts+1] = { 172 | tonumber(parts[2]), 173 | tonumber(parts[3]), 174 | tonumber(parts[4]), 175 | } 176 | elseif parts[1] == "mtllib" then 177 | materials = loadMTLFile(id, parts[2]) 178 | elseif parts[1] == "usemtl" then 179 | material = parts[2] 180 | elseif parts[1] == "f" then 181 | local faceVertices = {} 182 | local faceVTs = {} 183 | for _, part in pairs({parts[2], parts[3], parts[4], parts[5]}) do 184 | local parts2 = {} 185 | for part2 in part:gmatch("-?%d+") do 186 | parts2[#parts2+1] = part2 187 | end 188 | 189 | local vIndex = tonumber(parts2[1]) 190 | if vIndex > 0 then 191 | faceVertices[#faceVertices+1] = vertices[vIndex] 192 | else 193 | faceVertices[#faceVertices+1] = vertices[#vertices + vIndex + 1] 194 | end 195 | 196 | local vtIndex = tonumber(parts2[2]) 197 | if vtIndex > 0 then 198 | faceVTs[#faceVTs+1] = vts[vtIndex] 199 | else 200 | faceVTs[#faceVTs+1] = vts[#vts + vtIndex + 1] 201 | end 202 | end 203 | 204 | local v1 = faceVertices[1] 205 | local v2 = faceVertices[2] 206 | local v3 = faceVertices[3] 207 | local v4 = faceVertices[4] 208 | 209 | local x1 = v1[1] 210 | local y1 = v1[2] 211 | local z1 = v1[3] 212 | local x2 = v2[1] 213 | local y2 = v2[2] 214 | local z2 = v2[3] 215 | local x3 = v3[1] 216 | local y3 = v3[2] 217 | local z3 = v3[3] 218 | local x4, y4, z4 = nil, nil, nil 219 | if v4 then 220 | x4, y4, z4 = v4[1], v4[2], v4[3] 221 | end 222 | 223 | local poly = { 224 | x1 = x1, 225 | y1 = y1, 226 | z1 = z1, 227 | x2 = x2, 228 | y2 = y2, 229 | z2 = z2, 230 | x3 = x3, 231 | y3 = y3, 232 | z3 = z3, 233 | } 234 | 235 | if material:sub(-3) == "[F]" then 236 | poly.forceRender = true 237 | end 238 | 239 | do 240 | local v1 = faceVTs[1] 241 | local v2 = faceVTs[2] 242 | local v3 = faceVTs[3] 243 | 244 | local avgVTX = (v1[1] + v2[1] + v3[1]) / 3 245 | local avgVTY = (v1[2] + v2[2] + v3[2]) / 3 246 | 247 | local mat = materials.map[material] 248 | local texture = mat.texture 249 | if texture then 250 | local width = #texture[1] 251 | local height = #texture 252 | local textureX = (math.floor(avgVTX * width) % width) + 1 253 | local textureY = (math.floor(avgVTY * height) % height) + 1 254 | local color = texture[height - textureY + 1][textureX] 255 | poly.c = colorChar[color] or colors.red 256 | else 257 | if mat.baseColor then 258 | poly.c = mat.baseColor 259 | else 260 | if not materialsWarned[material] then 261 | term.setTextColor(colors.orange) 262 | print("Warning: no texture found for material \"" .. material .. "\"") 263 | term.setTextColor(colors.white) 264 | materialsWarned[material] = true 265 | end 266 | poly.c = colors.red 267 | end 268 | end 269 | end 270 | 271 | convertedModel[#convertedModel+1] = poly 272 | 273 | if v4 then 274 | if not quadsWarned then 275 | term.setTextColor(colors.orange) 276 | print("Warning: model contains quads which will be converted to two triangles each, making this model less efficient!") 277 | term.setTextColor(colors.white) 278 | quadsWarned = true 279 | end 280 | 281 | local poly2 = { 282 | x1 = x4, 283 | y1 = y4, 284 | z1 = z4, 285 | x3 = x3, 286 | y3 = y3, 287 | z3 = z3, 288 | x2 = x1, 289 | y2 = y1, 290 | z2 = z1, 291 | c = poly.c, 292 | } 293 | 294 | local v1 = faceVTs[1] 295 | local v4 = faceVTs[4] 296 | local v3 = faceVTs[3] 297 | 298 | local avgVTX = (v1[1] + v4[1] + v3[1]) / 3 299 | local avgVTY = (v1[2] + v4[2] + v3[2]) / 3 300 | 301 | local mat = materials.map[material] 302 | local texture = mat.texture 303 | if texture then 304 | local width = #texture[1] 305 | local height = #texture 306 | local textureX = (math.floor(avgVTX * width) % width) + 1 307 | local textureY = (math.floor(avgVTY * height) % height) + 1 308 | local color = texture[height - textureY + 1][textureX] 309 | poly.c = colorChar[color] or colors.red 310 | else 311 | if mat.baseColor then 312 | poly.c = mat.baseColor 313 | else 314 | if not materialsWarned[material] then 315 | term.setTextColor(colors.orange) 316 | print("Warning: no texture found for material \"" .. material .. "\"") 317 | term.setTextColor(colors.white) 318 | materialsWarned[material] = true 319 | end 320 | poly.c = colors.red 321 | end 322 | end 323 | 324 | convertedModel[#convertedModel+1] = poly2 325 | end 326 | 327 | if #convertedModel % 10000 == 0 then 328 | print("Processed " .. (#convertedModel) .. " polygons...") 329 | end 330 | end 331 | end 332 | 333 | print("Finished loading the .obj model!") 334 | 335 | return convertedModel 336 | end 337 | 338 | return { 339 | load = loadObjFile, 340 | } 341 | -------------------------------------------------------------------------------- /models/box: -------------------------------------------------------------------------------- 1 | { 2 | { 3 | y3 = -0.5, 4 | x3 = -0.5, 5 | z3 = 0.5, 6 | x2 = 0.5, 7 | y2 = -0.5, 8 | z2 = 0.5, 9 | z1 = -0.5, 10 | y1 = -0.5, 11 | x1 = -0.5, 12 | c = 16384, 13 | }, 14 | { 15 | y3 = -0.5, 16 | x3 = 0.5, 17 | z3 = 0.5, 18 | x2 = 0.5, 19 | y2 = -0.5, 20 | z2 = -0.5, 21 | z1 = -0.5, 22 | y1 = -0.5, 23 | x1 = -0.5, 24 | c = 2, 25 | }, 26 | { 27 | y3 = 0.5, 28 | x3 = 0.5, 29 | z3 = 0.5, 30 | x2 = -0.5, 31 | y2 = 0.5, 32 | z2 = 0.5, 33 | z1 = -0.5, 34 | y1 = 0.5, 35 | x1 = -0.5, 36 | c = 16, 37 | }, 38 | { 39 | y3 = 0.5, 40 | x3 = 0.5, 41 | z3 = -0.5, 42 | x2 = 0.5, 43 | y2 = 0.5, 44 | z2 = 0.5, 45 | z1 = -0.5, 46 | y1 = 0.5, 47 | x1 = -0.5, 48 | c = 32, 49 | }, 50 | { 51 | y3 = 0.5, 52 | x3 = -0.5, 53 | z3 = 0.5, 54 | x2 = -0.5, 55 | y2 = -0.5, 56 | z2 = 0.5, 57 | z1 = -0.5, 58 | y1 = -0.5, 59 | x1 = -0.5, 60 | c = 8192, 61 | }, 62 | { 63 | y3 = 0.5, 64 | x3 = -0.5, 65 | z3 = -0.5, 66 | x2 = -0.5, 67 | y2 = 0.5, 68 | z2 = 0.5, 69 | z1 = -0.5, 70 | y1 = -0.5, 71 | x1 = -0.5, 72 | c = 512, 73 | }, 74 | { 75 | y3 = -0.5, 76 | x3 = 0.5, 77 | z3 = 0.5, 78 | x2 = 0.5, 79 | y2 = 0.5, 80 | z2 = 0.5, 81 | z1 = -0.5, 82 | y1 = -0.5, 83 | x1 = 0.5, 84 | c = 2048, 85 | }, 86 | { 87 | y3 = 0.5, 88 | x3 = 0.5, 89 | z3 = 0.5, 90 | x2 = 0.5, 91 | y2 = 0.5, 92 | z2 = -0.5, 93 | z1 = -0.5, 94 | y1 = -0.5, 95 | x1 = 0.5, 96 | c = 8, 97 | }, 98 | { 99 | y3 = -0.5, 100 | x3 = 0.5, 101 | z3 = -0.5, 102 | x2 = 0.5, 103 | y2 = 0.5, 104 | z2 = -0.5, 105 | z1 = -0.5, 106 | y1 = -0.5, 107 | x1 = -0.5, 108 | c = 4, 109 | }, 110 | { 111 | y3 = 0.5, 112 | x3 = 0.5, 113 | z3 = -0.5, 114 | x2 = -0.5, 115 | y2 = 0.5, 116 | z2 = -0.5, 117 | z1 = -0.5, 118 | y1 = -0.5, 119 | x1 = -0.5, 120 | c = 1024, 121 | }, 122 | { 123 | y3 = 0.5, 124 | x3 = 0.5, 125 | z3 = 0.5, 126 | x2 = 0.5, 127 | y2 = -0.5, 128 | z2 = 0.5, 129 | z1 = 0.5, 130 | y1 = -0.5, 131 | x1 = -0.5, 132 | c = 64, 133 | }, 134 | { 135 | y3 = 0.5, 136 | x3 = -0.5, 137 | z3 = 0.5, 138 | x2 = 0.5, 139 | y2 = 0.5, 140 | z2 = 0.5, 141 | z1 = 0.5, 142 | y1 = -0.5, 143 | x1 = -0.5, 144 | c = 4096, 145 | }, 146 | } -------------------------------------------------------------------------------- /models/emerald: -------------------------------------------------------------------------------- 1 | { 2 | { 3 | y3 = 0.3, 4 | x3 = 0, 5 | z3 = 0, 6 | x2 = -0.3, 7 | y2 = -0.1, 8 | z2 = 0.3, 9 | z1 = -0.3, 10 | y1 = -0.1, 11 | x1 = -0.3, 12 | c = 32, 13 | }, 14 | { 15 | y3 = 0.3, 16 | x3 = 0, 17 | z3 = 0, 18 | x2 = 0.3, 19 | y2 = -0.1, 20 | z2 = 0.3, 21 | z1 = 0.3, 22 | y1 = -0.1, 23 | x1 = -0.3, 24 | c = 8192, 25 | }, 26 | { 27 | y3 = 0.3, 28 | x3 = 0, 29 | z3 = 0, 30 | x2 = 0.3, 31 | y2 = -0.1, 32 | z2 = -0.3, 33 | z1 = 0.3, 34 | y1 = -0.1, 35 | x1 = 0.3, 36 | c = 32, 37 | }, 38 | { 39 | y3 = 0.3, 40 | x3 = 0, 41 | z3 = 0, 42 | x2 = -0.3, 43 | y2 = -0.1, 44 | z2 = -0.3, 45 | z1 = -0.3, 46 | y1 = -0.1, 47 | x1 = 0.3, 48 | c = 8192, 49 | }, 50 | { 51 | y3 = -0.1, 52 | x3 = -0.3, 53 | z3 = 0.3, 54 | x2 = 0, 55 | y2 = -0.5, 56 | z2 = 0, 57 | z1 = -0.3, 58 | y1 = -0.1, 59 | x1 = -0.3, 60 | c = 8192, 61 | }, 62 | { 63 | y3 = -0.1, 64 | x3 = 0.3, 65 | z3 = 0.3, 66 | x2 = 0, 67 | y2 = -0.5, 68 | z2 = 0, 69 | z1 = 0.3, 70 | y1 = -0.1, 71 | x1 = -0.3, 72 | c = 32, 73 | }, 74 | { 75 | y3 = -0.1, 76 | x3 = 0.3, 77 | z3 = -0.3, 78 | x2 = 0, 79 | y2 = -0.5, 80 | z2 = 0, 81 | z1 = 0.3, 82 | y1 = -0.1, 83 | x1 = 0.3, 84 | c = 8192, 85 | }, 86 | { 87 | y3 = -0.1, 88 | x3 = -0.3, 89 | z3 = -0.3, 90 | x2 = 0, 91 | y2 = -0.5, 92 | z2 = 0, 93 | z1 = -0.3, 94 | y1 = -0.1, 95 | x1 = 0.3, 96 | c = 32, 97 | }, 98 | } -------------------------------------------------------------------------------- /models/pineapple: -------------------------------------------------------------------------------- 1 | { 2 | { 3 | y3 = 0.3, 4 | x3 = -0.4, 5 | z3 = 0.4, 6 | x2 = 0.4, 7 | y2 = 0.3, 8 | z2 = 0.4, 9 | z1 = 0, 10 | y1 = 0, 11 | x1 = 0, 12 | c = 16, 13 | }, 14 | { 15 | y3 = 0.3, 16 | x3 = 0.4, 17 | z3 = 0.4, 18 | x2 = 0.4, 19 | y2 = 0.3, 20 | z2 = -0.4, 21 | z1 = 0, 22 | y1 = 0, 23 | x1 = 0, 24 | c = 2, 25 | }, 26 | { 27 | y3 = 0.3, 28 | x3 = 0.4, 29 | z3 = -0.4, 30 | x2 = -0.4, 31 | y2 = 0.3, 32 | z2 = -0.4, 33 | z1 = 0, 34 | y1 = 0, 35 | x1 = 0, 36 | c = 16, 37 | }, 38 | { 39 | y3 = 0.3, 40 | x3 = -0.4, 41 | z3 = -0.4, 42 | x2 = -0.4, 43 | y2 = 0.3, 44 | z2 = 0.4, 45 | z1 = 0, 46 | y1 = 0, 47 | x1 = 0, 48 | c = 2, 49 | }, 50 | { 51 | y3 = 0.3, 52 | x3 = 0.4, 53 | z3 = 0.4, 54 | x2 = -0.4, 55 | y2 = 0.3, 56 | z2 = 0.4, 57 | z1 = 0, 58 | y1 = 0.8, 59 | x1 = 0, 60 | c = 2, 61 | }, 62 | { 63 | y3 = 0.3, 64 | x3 = 0.4, 65 | z3 = -0.4, 66 | x2 = 0.4, 67 | y2 = 0.3, 68 | z2 = 0.4, 69 | z1 = 0, 70 | y1 = 0.8, 71 | x1 = 0, 72 | c = 16, 73 | }, 74 | { 75 | y3 = 0.3, 76 | x3 = -0.4, 77 | z3 = -0.4, 78 | x2 = 0.4, 79 | y2 = 0.3, 80 | z2 = -0.4, 81 | z1 = 0, 82 | y1 = 0.8, 83 | x1 = 0, 84 | c = 2, 85 | }, 86 | { 87 | y3 = 0.3, 88 | x3 = -0.4, 89 | z3 = 0.4, 90 | x2 = -0.4, 91 | y2 = 0.3, 92 | z2 = -0.4, 93 | z1 = 0, 94 | y1 = 0.8, 95 | x1 = 0, 96 | c = 16, 97 | }, 98 | { 99 | y3 = 1, 100 | x3 = 0.3, 101 | char = "\127", 102 | z3 = 0.3, 103 | c = 32, 104 | forceRender = true, 105 | z1 = 0, 106 | y1 = 0.8, 107 | x1 = 0, 108 | charc = 8192, 109 | x2 = -0.3, 110 | y2 = 1, 111 | z2 = 0.3, 112 | }, 113 | { 114 | y3 = 1, 115 | x3 = -0.3, 116 | char = "\127", 117 | z3 = -0.3, 118 | c = 8192, 119 | forceRender = true, 120 | z1 = 0, 121 | y1 = 0.8, 122 | x1 = 0, 123 | charc = 32, 124 | x2 = 0.3, 125 | y2 = 1, 126 | z2 = -0.3, 127 | }, 128 | } -------------------------------------------------------------------------------- /models/pinetree: -------------------------------------------------------------------------------- 1 | { 2 | { 3 | x1 = 0.024359, 4 | y1 = 0.409, 5 | x2 = 0.168972, 6 | y2 = 0.67729, 7 | x3 = 0.359702, 8 | y3 = 0.409, 9 | c = 32, 10 | z1 = -0.401284, 11 | z3 = -0.179547, 12 | z2 = -0.094539, 13 | }, 14 | { 15 | x1 = 0.359702, 16 | y1 = 0.409, 17 | x2 = 0.157465, 18 | y2 = 0.673543, 19 | x3 = 0.335362, 20 | y3 = 0.40911, 21 | c = 32, 22 | z1 = -0.179547, 23 | z3 = 0.221175, 24 | z2 = 0.087697, 25 | }, 26 | { 27 | x1 = 0.335362, 28 | y1 = 0.40911, 29 | x2 = -0.010621, 30 | y2 = 0.671233, 31 | x3 = -0.02432, 32 | y3 = 0.409071, 33 | c = 32, 34 | z1 = 0.221175, 35 | z3 = 0.399621, 36 | z2 = 0.168564, 37 | }, 38 | { 39 | x1 = -0.010621, 40 | y1 = 0.671233, 41 | x2 = -0.359686, 42 | y2 = 0.409096, 43 | x3 = -0.02432, 44 | y3 = 0.409071, 45 | c = 32, 46 | z1 = 0.168564, 47 | z3 = 0.399621, 48 | z2 = 0.179091, 49 | }, 50 | { 51 | x1 = -0.167135, 52 | y1 = 0.673862, 53 | x2 = -0.335343, 54 | y2 = 0.409, 55 | x3 = -0.359686, 56 | y3 = 0.409096, 57 | c = 32, 58 | z1 = 0.067625, 59 | z3 = 0.179091, 60 | z2 = -0.221737, 61 | }, 62 | { 63 | x1 = -0.155638, 64 | y1 = 0.677478, 65 | x2 = 0.024359, 66 | y2 = 0.409, 67 | x3 = -0.335343, 68 | y3 = 0.409, 69 | c = 32, 70 | z1 = -0.114544, 71 | z3 = -0.221737, 72 | z2 = -0.401284, 73 | }, 74 | { 75 | x1 = 0.098925, 76 | y1 = 0.904855, 77 | x2 = 0.097066, 78 | y2 = 1.116233, 79 | x3 = 0.205655, 80 | y3 = 0.896129, 81 | c = 32, 82 | z1 = -0.187905, 83 | z3 = -0.017818, 84 | z2 = 0.000386, 85 | }, 86 | { 87 | x1 = 0.205655, 88 | y1 = 0.896129, 89 | x2 = 0.051262, 90 | y2 = 1.107125, 91 | x3 = 0.108157, 92 | y3 = 0.88517, 93 | c = 32, 94 | z1 = -0.017818, 95 | z3 = 0.156762, 96 | z2 = 0.082714, 97 | }, 98 | { 99 | x1 = 0.108157, 100 | y1 = 0.88517, 101 | x2 = -0.044088, 102 | y2 = 1.106638, 103 | x3 = -0.095972, 104 | y3 = 0.884541, 105 | c = 32, 106 | z1 = 0.156762, 107 | z3 = 0.161453, 108 | z2 = 0.084719, 109 | }, 110 | { 111 | x1 = -0.044088, 112 | y1 = 1.106638, 113 | x2 = -0.2026, 114 | y2 = 0.895009, 115 | x3 = -0.095972, 116 | y3 = 0.884541, 117 | c = 32, 118 | z1 = 0.084719, 119 | z3 = 0.161453, 120 | z2 = -0.008258, 121 | }, 122 | { 123 | x1 = -0.093633, 124 | y1 = 1.115264, 125 | x2 = -0.1052, 126 | y2 = 0.90436, 127 | x3 = -0.2026, 128 | y3 = 0.895009, 129 | c = 32, 130 | z1 = 0.00442, 131 | z3 = -0.008258, 132 | z2 = -0.18304, 133 | }, 134 | { 135 | x1 = -0.047845, 136 | y1 = 1.124111, 137 | x2 = 0.098925, 138 | y2 = 0.904855, 139 | x3 = -0.1052, 140 | y3 = 0.90436, 141 | c = 32, 142 | z1 = -0.077912, 143 | z3 = -0.18304, 144 | z2 = -0.187905, 145 | }, 146 | { 147 | x1 = 0.024359, 148 | y1 = 0.409, 149 | x2 = 0.012385, 150 | y2 = 0.678647, 151 | x3 = 0.168972, 152 | y3 = 0.67729, 153 | c = 32, 154 | z1 = -0.401284, 155 | z3 = -0.094539, 156 | z2 = -0.195831, 157 | }, 158 | { 159 | x1 = 0.359702, 160 | y1 = 0.409, 161 | x2 = 0.168972, 162 | y2 = 0.67729, 163 | x3 = 0.157465, 164 | y3 = 0.673543, 165 | c = 32, 166 | z1 = -0.179547, 167 | z3 = 0.087697, 168 | z2 = -0.094539, 169 | }, 170 | { 171 | x1 = 0.335362, 172 | y1 = 0.40911, 173 | x2 = 0.157465, 174 | y2 = 0.673543, 175 | x3 = -0.010621, 176 | y3 = 0.671233, 177 | c = 32, 178 | z1 = 0.221175, 179 | z3 = 0.168564, 180 | z2 = 0.087697, 181 | }, 182 | { 183 | x1 = -0.010621, 184 | y1 = 0.671233, 185 | x2 = -0.167135, 186 | y2 = 0.673862, 187 | x3 = -0.359686, 188 | y3 = 0.409096, 189 | c = 32, 190 | z1 = 0.168564, 191 | z3 = 0.179091, 192 | z2 = 0.067625, 193 | }, 194 | { 195 | x1 = -0.167135, 196 | y1 = 0.673862, 197 | x2 = -0.155638, 198 | y2 = 0.677478, 199 | x3 = -0.335343, 200 | y3 = 0.409, 201 | c = 32, 202 | z1 = 0.067625, 203 | z3 = -0.221737, 204 | z2 = -0.114544, 205 | }, 206 | { 207 | x1 = -0.155638, 208 | y1 = 0.677478, 209 | x2 = 0.012385, 210 | y2 = 0.678647, 211 | x3 = 0.024359, 212 | y3 = 0.409, 213 | c = 32, 214 | z1 = -0.114544, 215 | z3 = -0.401284, 216 | z2 = -0.195831, 217 | }, 218 | { 219 | x1 = 0.098925, 220 | y1 = 0.904855, 221 | x2 = 0.047505, 222 | y2 = 1.124594, 223 | x3 = 0.097066, 224 | y3 = 1.116233, 225 | c = 32, 226 | z1 = -0.187905, 227 | z3 = 0.000386, 228 | z2 = -0.079942, 229 | }, 230 | { 231 | x1 = 0.205655, 232 | y1 = 0.896129, 233 | x2 = 0.097066, 234 | y2 = 1.116233, 235 | x3 = 0.051262, 236 | y3 = 1.107125, 237 | c = 32, 238 | z1 = -0.017818, 239 | z3 = 0.082714, 240 | z2 = 0.000386, 241 | }, 242 | { 243 | x1 = 0.108157, 244 | y1 = 0.88517, 245 | x2 = 0.051262, 246 | y2 = 1.107125, 247 | x3 = -0.044088, 248 | y3 = 1.106638, 249 | c = 32, 250 | z1 = 0.156762, 251 | z3 = 0.084719, 252 | z2 = 0.082714, 253 | }, 254 | { 255 | x1 = -0.044088, 256 | y1 = 1.106638, 257 | x2 = -0.093633, 258 | y2 = 1.115264, 259 | x3 = -0.2026, 260 | y3 = 0.895009, 261 | c = 32, 262 | z1 = 0.084719, 263 | z3 = -0.008258, 264 | z2 = 0.00442, 265 | }, 266 | { 267 | x1 = -0.093633, 268 | y1 = 1.115264, 269 | x2 = -0.047845, 270 | y2 = 1.124111, 271 | x3 = -0.1052, 272 | y3 = 0.90436, 273 | c = 32, 274 | z1 = 0.00442, 275 | z3 = -0.18304, 276 | z2 = -0.077912, 277 | }, 278 | { 279 | x1 = -0.047845, 280 | y1 = 1.124111, 281 | x2 = 0.047505, 282 | y2 = 1.124594, 283 | x3 = 0.098925, 284 | y3 = 0.904855, 285 | c = 32, 286 | z1 = -0.077912, 287 | z3 = -0.187905, 288 | z2 = -0.079942, 289 | }, 290 | { 291 | x1 = 0, 292 | y1 = 0.281357, 293 | x2 = 0.086603, 294 | y2 = 0, 295 | x3 = 0, 296 | y3 = 0, 297 | c = 4096, 298 | z1 = -0.1, 299 | z3 = -0.1, 300 | z2 = 0.05, 301 | }, 302 | { 303 | x1 = 0.086603, 304 | y1 = 0.281357, 305 | x2 = -0.086603, 306 | y2 = 0, 307 | x3 = 0.086603, 308 | y3 = 0, 309 | c = 4096, 310 | z1 = 0.05, 311 | z3 = 0.05, 312 | z2 = 0.05, 313 | }, 314 | { 315 | x1 = -0.086603, 316 | y1 = 0.281357, 317 | x2 = 0, 318 | y2 = 0, 319 | x3 = -0.086603, 320 | y3 = 0, 321 | c = 4096, 322 | z1 = 0.05, 323 | z3 = 0.05, 324 | z2 = -0.1, 325 | }, 326 | { 327 | x1 = 0, 328 | y1 = 0.281357, 329 | x2 = 0.086603, 330 | y2 = 0.281357, 331 | x3 = 0.086603, 332 | y3 = 0, 333 | c = 4096, 334 | z1 = -0.1, 335 | z3 = 0.05, 336 | z2 = 0.05, 337 | }, 338 | { 339 | x1 = 0.086603, 340 | y1 = 0.281357, 341 | x2 = -0.086603, 342 | y2 = 0.281357, 343 | x3 = -0.086603, 344 | y3 = 0, 345 | c = 4096, 346 | z1 = 0.05, 347 | z3 = 0.05, 348 | z2 = 0.05, 349 | }, 350 | { 351 | x1 = -0.086603, 352 | y1 = 0.281357, 353 | x2 = 0, 354 | y2 = 0.281357, 355 | x3 = 0, 356 | y3 = 0, 357 | c = 4096, 358 | z1 = 0.05, 359 | z3 = -0.1, 360 | z2 = -0.1, 361 | }, 362 | { 363 | x1 = 6e-06, 364 | y1 = 0.417054, 365 | x2 = 0.422955, 366 | y2 = 0.222622, 367 | x3 = -0, 368 | y3 = 0.223002, 369 | c = 8192, 370 | z1 = -0.252408, 371 | z3 = -0.489222, 372 | z2 = -0.2459, 373 | }, 374 | { 375 | x1 = 0.422955, 376 | y1 = 0.222622, 377 | x2 = 0.218765, 378 | y2 = 0.417641, 379 | x3 = 0.423678, 380 | y3 = 0.223002, 381 | c = 8192, 382 | z1 = -0.2459, 383 | z3 = 0.244611, 384 | z2 = 0.123744, 385 | }, 386 | { 387 | x1 = 0.423678, 388 | y1 = 0.223002, 389 | x2 = 0.000126, 390 | y2 = 0.41758, 391 | x3 = -0, 392 | y3 = 0.223002, 393 | c = 8192, 394 | z1 = 0.244611, 395 | z3 = 0.489222, 396 | z2 = 0.248439, 397 | }, 398 | { 399 | x1 = 0.000126, 400 | y1 = 0.41758, 401 | x2 = -0.423678, 402 | y2 = 0.223002, 403 | x3 = -0, 404 | y3 = 0.223002, 405 | c = 8192, 406 | z1 = 0.248439, 407 | z3 = 0.489222, 408 | z2 = 0.244611, 409 | }, 410 | { 411 | x1 = -0.218567, 412 | y1 = 0.417643, 413 | x2 = -0.422955, 414 | y2 = 0.222622, 415 | x3 = -0.423678, 416 | y3 = 0.223002, 417 | c = 8192, 418 | z1 = 0.123575, 419 | z3 = 0.244611, 420 | z2 = -0.2459, 421 | }, 422 | { 423 | x1 = -0.422955, 424 | y1 = 0.222622, 425 | x2 = 6e-06, 426 | y2 = 0.417054, 427 | x3 = -0, 428 | y3 = 0.223002, 429 | c = 8192, 430 | z1 = -0.2459, 431 | z3 = -0.489222, 432 | z2 = -0.252408, 433 | }, 434 | { 435 | x1 = 0.071446, 436 | y1 = 0.661997, 437 | x2 = 0.132826, 438 | y2 = 0.945652, 439 | x3 = 0.281192, 440 | y3 = 0.660835, 441 | c = 8192, 442 | z1 = -0.286852, 443 | z3 = -0.088228, 444 | z2 = -0.047746, 445 | }, 446 | { 447 | x1 = 0.281192, 448 | y1 = 0.660835, 449 | x2 = 0.099258, 450 | y2 = 0.936144, 451 | x3 = 0.210337, 452 | y3 = 0.656496, 453 | c = 8192, 454 | z1 = -0.088228, 455 | z3 = 0.18735, 456 | z2 = 0.081147, 457 | }, 458 | { 459 | x1 = 0.210337, 460 | y1 = 0.656496, 461 | x2 = -0.031894, 462 | y2 = 0.933113, 463 | x3 = -0.070196, 464 | y3 = 0.65448, 465 | c = 8192, 466 | z1 = 0.18735, 467 | z3 = 0.264552, 468 | z2 = 0.116938, 469 | }, 470 | { 471 | x1 = -0.031894, 472 | y1 = 0.933113, 473 | x2 = -0.279777, 474 | y2 = 0.658627, 475 | x3 = -0.070196, 476 | y3 = 0.65448, 477 | c = 8192, 478 | z1 = 0.116938, 479 | z3 = 0.264552, 480 | z2 = 0.067013, 481 | }, 482 | { 483 | x1 = -0.129452, 484 | y1 = 0.940035, 485 | x2 = -0.209, 486 | y2 = 0.661642, 487 | x3 = -0.279777, 488 | y3 = 0.658627, 489 | c = 8192, 490 | z1 = 0.023961, 491 | z3 = 0.067013, 492 | z2 = -0.208847, 493 | }, 494 | { 495 | x1 = -0.095908, 496 | y1 = 0.949154, 497 | x2 = 0.071446, 498 | y2 = 0.661997, 499 | x3 = -0.209, 500 | y3 = 0.661642, 501 | c = 8192, 502 | z1 = -0.104933, 503 | z3 = -0.208847, 504 | z2 = -0.286852, 505 | }, 506 | { 507 | x1 = 0.109624, 508 | y1 = 1.096754, 509 | x2 = 0.003161, 510 | y2 = 1.367931, 511 | x3 = 0.137679, 512 | y3 = 1.076678, 513 | c = 8192, 514 | z1 = -0.095406, 515 | z3 = 0.042683, 516 | z2 = 0.047426, 517 | }, 518 | { 519 | x1 = 0.137679, 520 | y1 = 1.076678, 521 | x2 = 0.003161, 522 | y2 = 1.367931, 523 | x3 = 0.02937, 524 | y3 = 1.063678, 525 | c = 8192, 526 | z1 = 0.042683, 527 | z3 = 0.135356, 528 | z2 = 0.047426, 529 | }, 530 | { 531 | x1 = 0.02937, 532 | y1 = 1.063678, 533 | x2 = 0.003161, 534 | y2 = 1.367931, 535 | x3 = -0.106964, 536 | y3 = 1.071278, 537 | c = 8192, 538 | z1 = 0.135356, 539 | z3 = 0.090078, 540 | z2 = 0.047426, 541 | }, 542 | { 543 | x1 = -0.106964, 544 | y1 = 1.071278, 545 | x2 = 0.003161, 546 | y2 = 1.367931, 547 | x3 = -0.135006, 548 | y3 = 1.091583, 549 | c = 8192, 550 | z1 = 0.090078, 551 | z3 = -0.047925, 552 | z2 = 0.047426, 553 | }, 554 | { 555 | x1 = -0.135006, 556 | y1 = 1.091583, 557 | x2 = 0.003161, 558 | y2 = 1.367931, 559 | x3 = -0.026728, 560 | y3 = 1.104061, 561 | c = 8192, 562 | z1 = -0.047925, 563 | z3 = -0.140734, 564 | z2 = 0.047426, 565 | }, 566 | { 567 | x1 = -0.026728, 568 | y1 = 1.104061, 569 | x2 = 0.003161, 570 | y2 = 1.367931, 571 | x3 = 0.109624, 572 | y3 = 1.096754, 573 | c = 8192, 574 | z1 = -0.140734, 575 | z3 = -0.095406, 576 | z2 = 0.047426, 577 | }, 578 | { 579 | x1 = 6e-06, 580 | y1 = 0.417054, 581 | x2 = 0.218701, 582 | y2 = 0.41729, 583 | x3 = 0.422955, 584 | y3 = 0.222622, 585 | c = 8192, 586 | z1 = -0.252408, 587 | z3 = -0.2459, 588 | z2 = -0.126723, 589 | }, 590 | { 591 | x1 = 0.422955, 592 | y1 = 0.222622, 593 | x2 = 0.218701, 594 | y2 = 0.41729, 595 | x3 = 0.218765, 596 | y3 = 0.417641, 597 | c = 8192, 598 | z1 = -0.2459, 599 | z3 = 0.123744, 600 | z2 = -0.126723, 601 | }, 602 | { 603 | x1 = 0.423678, 604 | y1 = 0.223002, 605 | x2 = 0.218765, 606 | y2 = 0.417641, 607 | x3 = 0.000126, 608 | y3 = 0.41758, 609 | c = 8192, 610 | z1 = 0.244611, 611 | z3 = 0.248439, 612 | z2 = 0.123744, 613 | }, 614 | { 615 | x1 = 0.000126, 616 | y1 = 0.41758, 617 | x2 = -0.218567, 618 | y2 = 0.417643, 619 | x3 = -0.423678, 620 | y3 = 0.223002, 621 | c = 8192, 622 | z1 = 0.248439, 623 | z3 = 0.244611, 624 | z2 = 0.123575, 625 | }, 626 | { 627 | x1 = -0.218567, 628 | y1 = 0.417643, 629 | x2 = -0.218632, 630 | y2 = 0.417309, 631 | x3 = -0.422955, 632 | y3 = 0.222622, 633 | c = 8192, 634 | z1 = 0.123575, 635 | z3 = -0.2459, 636 | z2 = -0.126817, 637 | }, 638 | { 639 | x1 = -0.422955, 640 | y1 = 0.222622, 641 | x2 = -0.218632, 642 | y2 = 0.417309, 643 | x3 = 6e-06, 644 | y3 = 0.417054, 645 | c = 8192, 646 | z1 = -0.2459, 647 | z3 = -0.252408, 648 | z2 = -0.126817, 649 | }, 650 | { 651 | x1 = 0.071446, 652 | y1 = 0.661997, 653 | x2 = 0.035219, 654 | y2 = 0.951744, 655 | x3 = 0.132826, 656 | y3 = 0.945652, 657 | c = 8192, 658 | z1 = -0.286852, 659 | z3 = -0.047746, 660 | z2 = -0.140847, 661 | }, 662 | { 663 | x1 = 0.281192, 664 | y1 = 0.660835, 665 | x2 = 0.132826, 666 | y2 = 0.945652, 667 | x3 = 0.099258, 668 | y3 = 0.936144, 669 | c = 8192, 670 | z1 = -0.088228, 671 | z3 = 0.081147, 672 | z2 = -0.047746, 673 | }, 674 | { 675 | x1 = 0.210337, 676 | y1 = 0.656496, 677 | x2 = 0.099258, 678 | y2 = 0.936144, 679 | x3 = -0.031894, 680 | y3 = 0.933113, 681 | c = 8192, 682 | z1 = 0.18735, 683 | z3 = 0.116938, 684 | z2 = 0.081147, 685 | }, 686 | { 687 | x1 = -0.031894, 688 | y1 = 0.933113, 689 | x2 = -0.129452, 690 | y2 = 0.940035, 691 | x3 = -0.279777, 692 | y3 = 0.658627, 693 | c = 8192, 694 | z1 = 0.116938, 695 | z3 = 0.067013, 696 | z2 = 0.023961, 697 | }, 698 | { 699 | x1 = -0.129452, 700 | y1 = 0.940035, 701 | x2 = -0.095908, 702 | y2 = 0.949154, 703 | x3 = -0.209, 704 | y3 = 0.661642, 705 | c = 8192, 706 | z1 = 0.023961, 707 | z3 = -0.208847, 708 | z2 = -0.104933, 709 | }, 710 | { 711 | x1 = -0.095908, 712 | y1 = 0.949154, 713 | x2 = 0.035219, 714 | y2 = 0.951744, 715 | x3 = 0.071446, 716 | y3 = 0.661997, 717 | c = 8192, 718 | z1 = -0.104933, 719 | z3 = -0.286852, 720 | z2 = -0.140847, 721 | }, 722 | } -------------------------------------------------------------------------------- /noise.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Made by Xella#8655 3 | 4 | local function randomValue() 5 | return math.random(0, 1023) / 1023 6 | end 7 | 8 | local function getRawLayer(layerWidth, x, y, seed) 9 | math.randomseed(seed + x * 1000 + y * 1000000) 10 | local prelayer = {} 11 | for x = 1, layerWidth + 2 do 12 | prelayer[x] = {} 13 | for y = 1, layerWidth + 2 do 14 | local value = randomValue() 15 | prelayer[x][y] = value 16 | end 17 | end 18 | 19 | return prelayer 20 | end 21 | 22 | local function getValueLinear(X1, X2, Y1, Y2, X3) 23 | local a = (Y2 - Y1) / (X2 - X1) 24 | local b = Y2 - a * X2 25 | 26 | local Y3 = a * X3 + b 27 | 28 | return Y3 29 | end 30 | 31 | local function getValueCosine(X1, X2, Y1, Y2, X3) 32 | local Y3 = (1-math.cos(math.pi/(X1-X2) * (X3-X1))) / 2 * (Y2-Y1) + Y1 33 | 34 | return Y3 35 | end 36 | 37 | local function createNoiseLayer(size, layerWidth, x, y, seed) 38 | local prelayer = getRawLayer(layerWidth, x, y, seed) 39 | 40 | local prelayerUp = getRawLayer(layerWidth, x, y - 1, seed) 41 | local prelayerDown = getRawLayer(layerWidth, x, y + 1, seed) 42 | local prelayerLeft = getRawLayer(layerWidth, x - 1, y, seed) 43 | local prelayerRight = getRawLayer(layerWidth, x + 1, y, seed) 44 | 45 | for x = 2, layerWidth + 1 do 46 | prelayer[x][1] = prelayerUp[x][layerWidth + 1] 47 | end 48 | for x = 2, layerWidth + 1 do 49 | prelayer[x][layerWidth + 2] = prelayerDown[x][2] 50 | end 51 | for y = 2, layerWidth + 1 do 52 | prelayer[1][y] = prelayerLeft[layerWidth + 1][y] 53 | end 54 | for y = 2, layerWidth + 1 do 55 | prelayer[layerWidth + 2][y] = prelayerRight[2][y] 56 | end 57 | 58 | local layer = {} 59 | for x = 2, layerWidth + 1 do 60 | for y = 2, layerWidth + 1 do 61 | local value = prelayer[x][y] 62 | local valueUp = prelayer[x][y - 1] 63 | local valueDown = prelayer[x][y + 1] 64 | local valueLeft = prelayer[x - 1][y] 65 | local valueRight = prelayer[x + 1][y] 66 | 67 | for x2 = (x - 1 - 1) * size / layerWidth + 1, (x - 1) * size / layerWidth do 68 | if (layer[x2] == nil) then 69 | layer[x2] = {} 70 | end 71 | for y2 = (y - 1 - 1) * size / layerWidth + 1, (y - 1) * size / layerWidth do 72 | local localX = x2 - (x - 1 - 1) * size / layerWidth 73 | local localY = y2 - (y - 1 - 1) * size / layerWidth 74 | 75 | if localX == size / layerWidth / 2 and localY == size / layerWidth / 2 then 76 | layer[x2][y2] = value 77 | elseif localX - localY > 0 then -- upper right 78 | if size / layerWidth - localX - localY > 0 then -- upper left 79 | local X1 = -size / layerWidth / 2 80 | local X2 = size / layerWidth / 2 81 | local Y1 = valueUp 82 | local Y2 = value 83 | local X3 = localY 84 | 85 | local newValue = getValueCosine(X1, X2, Y1, Y2, X3) 86 | layer[x2][y2] = newValue -- up 87 | else -- bottom right 88 | local X1 = size / layerWidth / 2 89 | local X2 = size / layerWidth / 2 * 3 90 | local Y1 = value 91 | local Y2 = valueRight 92 | local X3 = localX 93 | 94 | local newValue = getValueCosine(X1, X2, Y1, Y2, X3) 95 | layer[x2][y2] = newValue -- right 96 | end 97 | else -- bottom left 98 | if (size / layerWidth - localX - localY > 0) then -- upper left 99 | local X1 = -size / layerWidth / 2 100 | local X2 = size / layerWidth / 2 101 | local Y1 = valueLeft 102 | local Y2 = value 103 | local X3 = localX 104 | 105 | local newValue = getValueCosine(X1, X2, Y1, Y2, X3) 106 | layer[x2][y2] = newValue -- left 107 | else -- bottom right 108 | local X1 = size / layerWidth / 2 109 | local X2 = size / layerWidth / 2 * 3 110 | local Y1 = value 111 | local Y2 = valueDown 112 | local X3 = localY 113 | 114 | local newValue = getValueCosine(X1, X2, Y1, Y2, X3) 115 | layer[x2][y2] = newValue -- bottom 116 | end 117 | end 118 | end 119 | end 120 | end 121 | end 122 | return layer 123 | end 124 | 125 | local function compressNoiseLayers(noiseLayers) 126 | local noise = {} 127 | noiseSize = #noiseLayers[1] 128 | 129 | for layerNr, layer in pairs(noiseLayers) do 130 | for x = 1, noiseSize do 131 | if not noise[x] then 132 | noise[x] = {} 133 | end 134 | for y = 1, noiseSize do 135 | if layerNr == 1 then 136 | noise[x][y] = layer[x][y] 137 | else 138 | noise[x][y] = (noise[x][y] * (layerNr - 1) + layer[x][y]) / layerNr 139 | end 140 | end 141 | end 142 | end 143 | 144 | return noise 145 | end 146 | 147 | function createNoise(size, x, y, seed, smoothness) 148 | local smoothness = smoothness or 1 149 | if not size then 150 | error("createNoise arg#1: integer expected, got nil") 151 | elseif type(size) ~= "number" then 152 | error("createNoise arg#1: integer expected, got "..type(size)) 153 | else 154 | if (size / (2 ^ smoothness)) / 2 % 1 ~= 0 then 155 | error("createNoise arg#1 and/or arg#5: the size must be at least 2 and divisible by 2^(smoothness+1)") 156 | end 157 | end 158 | if not x then 159 | error("createNoiseLayers arg#2 integer expected, got nil") 160 | elseif type(x) ~= "number" then 161 | error("createNoiseLayers arg#2: integer expected, got "..type(x)) 162 | end 163 | if not y then 164 | error("createNoiseLayers arg#3: integer expected, got nil") 165 | elseif type(y) ~= "number" then 166 | error("createNoiseLayers arg#3: integer expected, got "..type(y)) 167 | end 168 | if not seed then 169 | error("createNoiseLayers arg#4: integer expected, got nil") 170 | elseif type(seed) ~= "number" then 171 | error("createNoiseLayers arg#4: integer expected, got "..type(seed)) 172 | end 173 | 174 | local noiseLayers = {} 175 | 176 | local layerWidth = 2 177 | while layerWidth <= size / (2 ^ smoothness) do 178 | local layer = createNoiseLayer(size, layerWidth, x, y, seed) 179 | noiseLayers[#noiseLayers+1] = layer 180 | layerWidth = layerWidth * 2 181 | end 182 | 183 | local compressedNoise = compressNoiseLayers(noiseLayers) 184 | 185 | return compressedNoise 186 | end 187 | 188 | return { 189 | createNoise = createNoise, 190 | } 191 | --------------------------------------------------------------------------------