Skip to content

3D Fire

from gridmas import *
import math
import random

name = "3D Fire"
author = "Godzil"
# derived from https://github.com/standupmaths/xmastree2020/blob/main/examples/3dfire.py

# Play with these values to change how coarse the 3D Fire effect is.
# Smaller value == faster
MATWX = 10
MATWY = 10
MATWZ = 30

# Change that value to change colour brightness.
# May need to tweak the palette if changing that value
maxBrightness = 255


class boundingBox():
    def __init__(self):
        self.minX = math.inf
        self.maxX = -math.inf
        self.minY = math.inf
        self.maxY = -math.inf
        self.minZ = math.inf
        self.maxZ = -math.inf
        self.wX = 0
        self.wY = 0
        self.wZ = 0

    def update(self, x, y, z):
        if self.minX > x:
            self.minX = x
        if self.maxX < x:
            self.maxX = x

        if self.minY > y:
            self.minY = y
        if self.maxY < y:
            self.maxY = y

        if self.minZ > z:
            self.minZ = z
        if self.maxZ < z:
            self.maxZ = z

    def finalize(self):
        self.wX = self.maxX - self.minX
        self.wY = self.maxY - self.minY
        self.wZ = self.maxZ - self.minZ

    def normalize(self, x, y, z):
        lx = (x - self.minX) / self.wX
        ly = (y - self.minY) / self.wY
        lz = (z - self.minZ) / self.wZ
        return lx, ly, lz


class matrix():
    def __init__(self, lx, ly, lz, bb):
        self._list = [0] * lx * ly * lz
        self._strideX = 1
        self._strideY = self._strideX * lx
        self._strideZ = self._strideY * ly
        self._bb = bb
        self._wX = lx
        self._wY = ly
        self._wZ = lz

    def get(self, x, y, z):
        return self._list[x * self._strideX + y * self._strideY + z * self._strideZ]

    def set(self, x, y, z, val):
        self._list[x * self._strideX + y * self._strideY + z * self._strideZ] = val

    def copy(self, other):
        self._list = other._list[:]

    def getTree(self, x, y, z):
        localX, localY, localZ = self._bb.normalize(x, y, z)
        localX = int(localX * (self._wX - 1))
        localY = int(localY * (self._wY - 1))
        localZ = int(localZ * (self._wZ - 1))
        return self.get(localX, localY, localZ)

coords = coords()

def draw():
    # Color are G R B
    palette: list[tuple[int, int, int]] = []

    # Transition points
    palBST = 70
    palB2R = 86  # Black to Red
    palR2Y = 99  # Red to Yellow

    # Black only
    for i in range(0, palBST):
        palette.append((0, 0, 0))
    # Black to red
    for i in range(palBST, palB2R):
        palette.append((int((i - palBST) / (palB2R - palBST) * maxBrightness), 0, 0))
    # red to yellow
    for i in range(palB2R, palR2Y):
        palette.append((maxBrightness, int((i - palB2R) / (palR2Y - palB2R) * maxBrightness), 0))
    # yellow to white
    for i in range(palR2Y, 256):
        palette.append((255, 255, int((i - (palR2Y)) / (256 - palR2Y) * maxBrightness)))

    treeBB = boundingBox()
    for i in coords:
        treeBB.update(i[0], i[1], i[2])

    treeBB.finalize()

    # Our working area. We work with a non code/cylinder shape as it
    # would make thing too complicated
    workMat = matrix(MATWX, MATWY, MATWZ, treeBB)
    oldMat = matrix(MATWX, MATWY, MATWZ, treeBB)

    while True:

        for LED, pixel in enumerate(pixels()):
            v = workMat.getTree(coords[LED][0], coords[LED][1], coords[LED][2])
            pixel.set_rgb(*palette[v])

        yield

        oldMat.copy(workMat)

        # Update the matrix
        for x in range(1, MATWX - 1):
            for y in range(1, MATWY - 1):
                for z in range(2, MATWZ):
                    v = oldMat.get(x, y, z - 2)
                    v = v + oldMat.get(x - 1, y, z - 1)
                    v = v + oldMat.get(x, y - 1, z - 1)
                    v = v + oldMat.get(x, y, z - 1)
                    v = v + oldMat.get(x, y + 1, z - 1)
                    v = v + oldMat.get(x + 1, y, z - 1)
                    v = max(min(int(v / 6), 255), 0)

                    workMat.set(x, y, z, v)

        # light the fire!
        for x in range(0, MATWX):
            for y in range(0, MATWY):
                for z in range(0, 2):
                    if random.uniform(0, 1) < 0.35:
                        workMat.set(x, y, z, 255)
                    else:
                        workMat.set(x, y, z, 0)