rgbxmas
2022-12-21 ยท 21 min read
setup #
First, plug the USB cable into your machine (macOS, Windows, Linux, whatever). The board should power up and show the little demo; press the UP button to go to the next screen.
On your machine, a USB-drive called CIRCUITPY
should also pop up. This is the Adafruit Matrix Portal set up with CircuitPython ready for programming.
The Matrix Portal's got lots of features and room for other peripherals and sensors. it can even connect to stuff on the internet, so you can make a weather display, read some tweets, show the bitcoin price... : )
In the CIRCUITPY
directory, there should be a code.py
file. This contains the code for the demo, which is also listed below, see: demo code.
CircuitPython makes it super painless to write code for little microcontroller boards like this. It ain't gonna be the fastest, but it's pretty easy to get stated. To get some basic code going, you literally just edit the code.py
file, save it, and the board will auto-restart with the new updates.
I'd probably start with the Mu editor here: https://codewith.mu/en/download. It's bare-bones, but it'll work in the beginning.
I've already setup the board so everything should be good to go. With the board plugged in, just open the Mu editor, press the "Mode" icon, and select "CircuitPython". Then you should be able to edit the code, save, and see the changes appear on the device.
Also check out Adafruit's MatrixPortal overview: https://learn.adafruit.com/adafruit-matrixportal-m4/overview
and the CircuitPython Essentials guide: https://learn.adafruit.com/circuitpython-essentials/circuitpython-essentials
and the detailed CircuitPython core modules documentation: https://docs.circuitpython.org/en/latest/shared-bindings/index.html
blink the board LED #
Here's a basic example to get started: it blinks an LED on the board itself (not the display) on/off every 0.5 seconds.
# file: code.py
import board
import digitalio
import time
# This a the little red LED on the MatrixPortal m4 board,
# not the big LED matrix panel. It's located right above
# the "adafruit" logo.
led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT
# This block loops endlessly
while True:
# Turn on the LED, then wait for 0.5 seconds
led.value = True
time.sleep(0.5)
# Turn off the LED, then wait for 0.5 seconds
led.value = False
time.sleep(0.5)
getting help from the REPL #
The panel opened by the "Serial" button doesn't just show the print
output of the device, but also let's you interact with it. This is called a REPL (Read-Evaluate-Print-Loop) and lets you quickly play with bits of code or explore different modules.
To enter the REPL:
If your program has finished, you should see this message:
Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.
code.py output:
Code done running.
Press any key to enter the REPL. Use CTRL-D to reload.
In this case just press the "Enter" key in the Serial box and you should see the REPL pop up:
Adafruit CircuitPython 7.3.3 on 2022-08-29; Adafruit Matrix Portal M4 with samd51j19
>>>
If some code is running (like the infinite blink example above), you may need to hit CTRL-C to stop the program first (though it seems a bit flaky, I had to hold the keys down for a second or two to get it to work).
With the REPL open, we can see all the available built-in modules by typing "import " (with the space) then hitting the TAB key to auto-complete:
>>> import ^TAB
builtins micropython __future__ _asyncio
_bleio adafruit_bus_device
adafruit_bus_device.i2c_device adafruit_bus_device.spi_device
adafruit_pixelbuf alarm analogio
array atexit audiobusio audiocore
audioio audiomixer audiomp3 binascii
bitbangio bitmaptools board busio
collections countio digitalio displayio
errno fontio framebufferio frequencyio
gc getpass gifio i2cperipheral
io json keypad math
microcontroller msgpack neopixel_write
onewireio os ps2io pulseio
pwmio rainbowio random re
rgbmatrix rotaryio rtc samd
sdcardio select storage struct
supervisor synthio sys terminalio
time touchio traceback usb_cdc
usb_hid usb_midi uselect vectorio
watchdog zlib
Don't worry about what most of these do for now.
If you want to get very very detailed documentation about these modules, you can check the CircuitPython Modules reference.
Let's check out the time
module we used in the previous example:
>>> import time
>>> help(time)
object <module 'time'> is of type module
__name__ -- time
monotonic -- <function>
sleep -- <function>
struct_time -- <class 'struct_time'>
localtime -- <function>
mktime -- <function>
time -- <function>
monotonic_ns -- <function>
We can get the number of seconds since the board powered on by calling time.monotonic()
:
>>> time.monotonic()
1003.53
We use this one a lot in the demo.
From the REPL we can define functions and variables just like we expect:
>>> def f(x):
# f(x) = 4x^2 - 4x - 24
x_squared = x**2
return 4*x_squared - 4*x - 24
>>> f(0)
-24
>>> print("wow look f(3) is zero:", f(3))
We can exit the REPL again by hitting CTRL-D.
displaying text #
This example requires the adafruit_display_text
and adafruit_matrixportal
libraries, which are not built-in modules. Most interesting libraries are not built-in, as they are updated more frequently than the base modules and would otherwise take up too much space on the device.
To install these community libraries, go to https://circuitpython.org/libraries and download the zip file under "Bundle for Version 7.X" (I've installed CircuitPython version 7.3.3).
Opening the adafruit-circuitpython-bundle-7.x.zip
file there should be a lib
folder, with the adafruit_display_text
and adafruit_matrixportal
folders inside (among many others).
Copy the those two folders from the .zip
file to the mounted CIRCUITPY/lib/
folder to install the library.
import terminalio
from adafruit_display_text import label
from adafruit_matrixportal.matrix import Matrix
# This call creates the Matrix display object.
#
# `bit_depth` can range from 1 to 6; higher numbers allow
# for more color shades to be displayed, but increase memory
# usage and slow down your Python code.
#
# If you just want to show primary colors
# plus black and white, use 1. Otherwise, try 3, 4 and 5
# to see which effect you like best.
matrix = Matrix(
bit_depth=4,
)
display = matrix.display
# this is a text area with two lines like
# | Hello |
# | World |
text_area = label.Label(
terminalio.FONT,
text="Hello\nWorld", # '\n' stands for newline
color=0x113311, # this is the RGB (Red Green Blue) color
)
# this is the starting position of the text on the board,
# counting pixels from the top-left.
text_area.x = 16
text_area.y = 8
display.show(text_area)
# You can add more effects in this loop. For instance, maybe
# you want to set the color of each label to a different
# value
while True:
display.refresh()
demo code #
This demo shows a few screens with some cool graphics.
Note that it requires the adafruit_display_text
library is copied into the CIRCUITPY/lib/
directory. See displaying text for more details on libraries.
# code.py
import random
import time
import math
from math import trunc, sin, cos, sqrt
import board
import countio
import digitalio
import displayio
import framebufferio
import rgbmatrix
import terminalio
import vectorio
from adafruit_display_text import label
# create a new `Display` object for the big RGB matrix display
def make_display(bit_depth=6, brightness=0.5):
displayio.release_displays()
matrix = rgbmatrix.RGBMatrix(
width=64,
height=32,
bit_depth=bit_depth,
# NOTE: for some reason the board has reversed the green and blue pins,
# so we just swap them here to get standard RGB ordering
rgb_pins=[
board.MTX_R1,
board.MTX_B1,
board.MTX_G1,
board.MTX_R2,
board.MTX_B2,
board.MTX_G2,
],
addr_pins=[
board.MTX_ADDRA,
board.MTX_ADDRB,
board.MTX_ADDRC,
board.MTX_ADDRD,
],
clock_pin=board.MTX_CLK,
latch_pin=board.MTX_LAT,
output_enable_pin=board.MTX_OE
)
display = framebufferio.FramebufferDisplay(matrix)
display.brightness = brightness
return display
def rgb_to_int(r, g, b):
return (r << 16) | (g << 8) | b
def percent_to_byte(t):
return round(t * 255.0) & 255
def hue_to_rgb_ratio(p, q, t):
if t < 0.0:
t += 1.0
elif t > 1.0:
t -= 1.0
if t < 0.166667:
return p + (q - p) * 6.0 * t
elif t < 0.5:
return q
elif t < 0.666667:
return p + (q - p) * (0.666667 - t) * 6.0
else:
return p
# convert Hue-Saturation-Lightness to RGB color
# `h` is the Hue, in degrees (0 -> 360)
# `s` is the Saturation, between 0.0 and 1.0
# `l` is the Lightness, between 0.0 and 1.0
def hsl(h, s, l):
# all greys if saturation is 0
if s == 0.0:
l = percent_to_byte(l)
return rgb_to_int(l, l, l)
h = h / 360.0
q = 0.0
if l < 0.5:
q = l * (1.0 + s)
else:
q = l + s - (l * s)
p = 2.0 * l - q
r = percent_to_byte(hue_to_rgb_ratio(p, q, h + 0.333333))
g = percent_to_byte(hue_to_rgb_ratio(p, q, h))
b = percent_to_byte(hue_to_rgb_ratio(p, q, h - 0.333333))
return rgb_to_int(r, g, b)
# the fractional part of a float. ex: `frac(1.75) == 0.75`.
def frac(x):
return x - trunc(x)
# clamp `x` so the output is always between `x_min` and `x_max`.
def clamp(x_min, x_max, x):
return min(x_max, max(x_min, x))
# linearly interpolate between `start` and `end`, using `t` between 0.0 and 1.0
# as the "interpolation distance"
def lerp(start, end, t):
return t * start + (1.0 - t) * end
# saw-tooth signal, between y = 0.0 and 1.0, with period = 1.0
def saw(t):
return math.fabs(2.0 * frac(t) - 1.0)
# exponential ease in-out function
# see: <https://easings.net/#easeInOutExpo>
# this is defined for x between 0.0 and 1.0
def ease_in_out_exp(x):
if x < 0.5:
return 0.5 * math.pow(2.0, 20.0 * x - 10.0)
else:
return 1.0 - 0.5 * math.pow(2.0, -20.0 * x + 10.0)
# see: <https://mathworld.wolfram.com/SincFunction.html>
def sinc(x):
a = math.pi * x
return sin(a) / (a+0.001)
def parabola(x, k):
return math.pow(4.0 * x * (1.0 - x), k)
class Screen:
def __init__(self, display):
pass
def update(self, display):
pass
# shows "PRESS UP >>", with the ">>" bouncing every few seconds.
class StartScreen(Screen):
def __init__(self, display):
group = displayio.Group()
# the "PRESS UP" text
press_up = label.Label(terminalio.FONT, text="PRESS UP")
press_up.x = 4
press_up.y = 14
group.append(press_up)
# the two '>' arrows on the right
arrow_group = displayio.Group()
arrow_right_1 = label.Label(terminalio.FONT, text=">")
arrow_right_1.x = 52
arrow_right_1.y = 14
arrow_group.append(arrow_right_1)
arrow_right_2 = label.Label(terminalio.FONT, text=">")
arrow_right_2.x = 54
arrow_right_2.y = 14
arrow_group.append(arrow_right_2)
group.append(arrow_group)
display.show(group)
self.arrow_group = arrow_group
def update(self, display):
t = time.monotonic()
# self.arrow_group.x = round(2.0 * ease_in_out_exp(0.5 * sin(3.0 * t) + 0.5) - 4.0)
# t = math.fmod(t, 5.0)
# self.arrow_group.x = round(8.0 * sinc(0.54 * (t - 2.5)))
# repeat "bounce" every 3 sec
t = math.fmod(t, 3.0)
self.arrow_group.x = round(4.0 - 4.0 * parabola(0.333333 * t, 7))
display.refresh()
# makes a big circle wave across the screen in a loop
class CircleScreen(Screen):
def __init__(self, display):
group = displayio.Group()
palette = displayio.Palette(2)
palette[0] = hsl(215, 0.42, 0.34)
palette[1] = hsl(0, 0, 0.0)
circle_outer = vectorio.Circle(
pixel_shader=palette,
radius=16,
x=0,
y=0,
color_index=0,
)
group.append(circle_outer)
circle_inner = vectorio.Circle(
pixel_shader=palette,
radius=15,
x=0,
y=0,
color_index=1,
)
group.append(circle_inner)
display.show(group)
self.palette = palette
self.group = group
def update(self, display):
t = time.monotonic()
# make the circle wave across the screen
self.group.x = round(math.fmod(64.0 * t, 110.0) - 25.0)
self.group.y = int(8.0 * sin(3.0 * t)) + 16
# slowly rotate the hue of the circle over time
t_color = 0.5 * sin(1.0 * t) + 0.5
hue = lerp(0.0, 180.0, t_color)
self.palette[0] = hsl(hue, 0.75, 0.50)
display.refresh()
# test the display's color
class ColorTestScreen(Screen):
def __init__(self, display):
width = 64
height = 32
num_colors = 256
# start with all greyscale
palette = displayio.Palette(num_colors)
for idx in range(num_colors):
palette[idx] = rgb_to_int(idx, idx, idx)
# fill the bitmap with 0..=255
bm = displayio.Bitmap(width, height, num_colors)
for y in range(height):
for x in range(width):
# use the same color every 8 pixels in the y-direction
color_idx = (y // 8) * width + x
bm[x, y] = color_idx
bm.dirty()
tg = displayio.TileGrid(bm, pixel_shader=palette)
group = displayio.Group()
group.append(tg)
display.show(group)
self.num_colors = num_colors
self.palette = palette
self.prev_update_t = time.monotonic()
def update(self, display):
t = time.monotonic()
dt = t - self.prev_update_t
# every 5 sec change the color spectrum to a random hue and saturation
# with lightness evenly between 0.0 and 1.0
if dt > 5.0:
num_colors = self.num_colors
hue = random.randint(0, 360)
saturation = random.random()
print(f"hue: {hue}, saturation: {saturation}")
for idx in range(num_colors):
self.palette[idx] = hsl(hue, saturation, 0.5 * (idx / num_colors))
self.prev_update_t = t
display.refresh()
# run a random [Conway's Game of Life](https://www.wikiwand.com/en/Conway%27s_Game_of_Life)
# simulation every ~30 sec
class GameOfLifeScreen(Screen):
def __init__(self, display):
width = 64
height = 32
num_colors = 2
palette = displayio.Palette(num_colors)
bm1 = displayio.Bitmap(width, height, num_colors)
bm2 = displayio.Bitmap(width, height, num_colors)
tg1 = displayio.TileGrid(bm1, pixel_shader=palette)
tg2 = displayio.TileGrid(bm2, pixel_shader=palette)
g1 = displayio.Group()
g1.append(tg1)
g2 = displayio.Group()
g2.append(tg2)
display.show(g1)
self.width = width
self.height = height
self.iter = 0
self.reset_after = 200
self.curr = bm1
self.next = bm2
self.curr_group = g1
self.next_group = g2
self.palette = palette
def randomize(self):
curr = self.curr
for idx in range(self.width * self.height):
curr[idx] = random.random() < 0.333333
# hue = random.randint(0, 360)
# saturation = (random.random() / 4.0) + 0.5
# lightness = (random.random() / 3.0) + 0.1
# self.palette[1] = hsl(hue, saturation, lightness)
def apply_life_rule(self):
curr = self.curr
w = self.width
h = self.height
for y in range(h):
yyy = y * w
ym1 = ((y + h - 1) & 31) * w
yp1 = ((y + 1) & 31) * w
xm1 = w - 1
for x in range(w):
xp1 = (x + 1) & 63
neighbors = (
curr[xm1 + ym1] + curr[xm1 + yyy] + curr[xm1 + yp1] +
curr[ x + ym1] + curr[ x + yp1] +
curr[xp1 + ym1] + curr[xp1 + yyy] + curr[xp1 + yp1]
)
self.next[x + yyy] = (neighbors == 3) or ((neighbors == 2) and curr[x + yyy])
xm1 = x
self.curr = self.next
self.next = curr
curr_group = self.curr_group
self.curr_group = self.next_group
self.next_group = curr_group
def update(self, display):
t = time.monotonic()
# slowly change the color
t_color = 0.5 * sin(1.0 * t) + 0.5
hue = lerp(0.0, 180.0, t_color)
self.palette[1] = hsl(hue, 0.75, 0.50)
# reset and randomize the state every `self.reset_after` iterations
if self.iter == 0:
self.randomize()
else:
self.apply_life_rule()
self.iter = (self.iter + 1) % self.reset_after
display.show(self.curr_group)
# a cool wavy pattern
class WarpScreen(Screen):
def __init__(self, display):
width = 64
height = 32
num_colors = 64
palette = displayio.Palette(num_colors)
for idx in range(num_colors):
# palette[idx] = hsl(240, 0.8, 0.25 * (idx / num_colors))
palette[idx] = hsl(265, 0.85, 0.95 * (idx / num_colors) + 0.05)
bm1 = displayio.Bitmap(width, height, num_colors)
bm2 = displayio.Bitmap(width, height, num_colors)
tg1 = displayio.TileGrid(bm1, pixel_shader=palette)
tg2 = displayio.TileGrid(bm2, pixel_shader=palette)
g1 = displayio.Group()
g1.append(tg1)
g2 = displayio.Group()
g2.append(tg2)
# display.show(g1)
self.width = width
self.height = height
self.num_colors = num_colors
self.curr = bm1
self.next = bm2
self.curr_group = g1
self.next_group = g2
def update(self, display):
width = self.width
height = self.height
num_colors = self.num_colors
curr = self.curr
next = self.next
# render to next framebuffer while current one is shown
t = time.monotonic()
for y in range(height):
for x in range(width):
tx = 20.0 * sin(0.01 * x) + 0.2 * t
ty = 10.0 * sin(0.01 * y) + 0.2 * t
l = (0.5 * sin(tx) * sin(ty)) + 0.5
next[x, y] = round((num_colors - 1) * l)
# swap frame buffers
self.curr = next
self.next = curr
curr_group = self.curr_group
self.curr_group = self.next_group
self.next_group = curr_group
# show new framebuffer
display.show(self.curr_group)
def noise(u, v):
return sin(u) * sin(v)
# [ [ 0.8 -0.5 ]
# [ 0.5 0.8 ] ] dot [p_x, p_y] * 2
def mdp(x, y):
return ((1.6 * x) - (1.0 * y), (1.0 * x) + (1.6 * y))
def fbm4(x, y):
f = 0.0
f += 0.5000 * noise(x, y); (x, y) = mdp(x, y)
f += 0.2500 * noise(x, y); (x, y) = mdp(x, y)
f += 0.1250 * noise(x, y); (x, y) = mdp(x, y)
f += 0.0625 * noise(x, y)
return f / 0.9375
# another cool pattern that rotates a bit
# uses the same `sin(x)*sin(y)` but run through a few octaves of modified
# [Fractional Brownian Motion](https://www.wikiwand.com/en/Fractional_Brownian_motion)
class Warp2Screen(Screen):
def __init__(self, display):
width = 64
height = 32
num_colors = 64
palette = displayio.Palette(num_colors)
for idx in range(num_colors):
# palette[idx] = hsl(240, 0.8, 0.25 * (idx / num_colors))
palette[idx] = hsl(15, 0.8, 0.6 * (idx / num_colors))
bm1 = displayio.Bitmap(width, height, num_colors)
bm2 = displayio.Bitmap(width, height, num_colors)
tg1 = displayio.TileGrid(bm1, pixel_shader=palette)
tg2 = displayio.TileGrid(bm2, pixel_shader=palette)
g1 = displayio.Group()
g1.append(tg1)
g2 = displayio.Group()
g2.append(tg2)
# display.show(g1)
self.width = width
self.height = height
self.num_colors = num_colors
self.curr = bm1
self.next = bm2
self.curr_group = g1
self.next_group = g2
def update(self, display):
width = self.width
height = self.height
num_colors = self.num_colors
curr = self.curr
next = self.next
# render to next framebuffer while current one is shown
t = time.monotonic()
for y in range(height):
for x in range(width):
u = (x - width) / height
v = (y - height) / height
# add some back-and-forth rotation
coa = cos(0.10 * sin(0.15 * t))
sia = sin(0.31 * sin(0.10 * t))
u = (u * coa) - (v * sia)
v = (u * sia) + (v * coa)
u += 0.03 * sin(0.54 * t)
v += 0.03 * sin(0.46 * t)
# kinda fractional brownian motion over sin(x)*sin(y)
l = 0.5 * fbm4(8.0 * u, 8.0 * v) + 0.5
# make the lightness periodically show the dark spots
c = 0.1 * cos(0.2 * t) + 0.2
l = clamp(0.0, 1.0, l - c)
next[x, y] = round((num_colors - 1) * l)
# swap frame buffers
self.curr = next
self.next = curr
curr_group = self.curr_group
self.curr_group = self.next_group
self.next_group = curr_group
# show new framebuffer
display.show(self.curr_group)
# ||v||^2
def norm_sq(x, y, z):
return x*x + y*y + z*z
# ||v||
def norm(x, y, z):
return sqrt(norm_sq(x, y, z))
# v / ||v||
def normalize(x, y, z):
inv_l = 1.0 / norm(x, y, z)
return (inv_l * x, inv_l * y, inv_l * z)
# a * b
def dot(a_x, a_y, a_z, b_x, b_y, b_z):
return a_x*b_x + a_y*b_y + a_z*b_z
# a x b
def cross(bx, by, bz, cx, cy, cz):
return (by*cz - bz*cy, bz*cx - bx*cz, bx*cy - by*cx)
# sphere hit
def i_sphere(ro_x, ro_y, ro_z, rd_x, rd_y, rd_z, sph_x, sph_y, sph_z):
oc_x = ro_x - sph_x
oc_y = ro_y - sph_y
oc_z = ro_z - sph_z
b = dot(oc_x, oc_y, oc_z, rd_x, rd_y, rd_z)
c = norm_sq(oc_x, oc_y, oc_z) - 1.0
h = b*b - c
if h < 0.0:
return -1.0
else:
return -b - sqrt(h)
# sphere soft shadow
def ss_sphere(ro_x, ro_y, ro_z, rd_x, rd_y, rd_z, sph_x, sph_y, sph_z):
oc_x = sph_x - ro_x
oc_y = sph_y - ro_y
oc_z = sph_z - ro_z
b = dot(oc_x, oc_y, oc_z, rd_x, rd_y, rd_z)
if b > 0.0:
h = norm_sq(oc_x, oc_y, oc_z) - b*b - 1.0
return clamp(0.0, 1.0, 16.0 * h / b)
else:
return 1.0
# sphere lambert shading
def lambert(lig_x, lig_y, lig_z, nor_x, nor_y, nor_z):
return max(0.0, dot(lig_x, lig_y, lig_z, nor_x, nor_y, nor_z))
# sphere occlusion
def occ_sphere(pos_x, pos_y, pos_z, nor_x, nor_y, nor_z, sph_x, sph_y, sph_z):
di_x = sph_x - pos_x
di_y = sph_y - pos_y
di_z = sph_z - pos_z
l_sq = norm_sq(di_x, di_y, di_z)
inv_l = 1.0 / math.sqrt(l_sq)
di_x = di_x * inv_l
di_y = di_y * inv_l
di_z = di_z * inv_l
nd = dot(nor_x, nor_y, nor_z, di_x, di_y, di_z)
return 1.0 - (max(0.0, nd) / l_sq)
# this screen is a small [raytracer](https://www.wikiwand.com/en/Ray_tracing_(graphics))
# that renders two spheres with soft shadows and ambient occlusion, while
# rotating the camera around them.
class SpheresScreen(Screen):
def __init__(self, display):
width = 64
height = 32
num_colors = 64
palette = displayio.Palette(num_colors)
for idx in range(num_colors):
# palette[idx] = hsl(240, 0.8, 0.25 * (idx / num_colors))
# palette[idx] = hsl(lerp(240, 260, idx / num_colors), 1.0, 1.0 * (idx / num_colors))
palette[idx] = hsl(220, 0.9, 1.0 * (idx / num_colors))
bm1 = displayio.Bitmap(width, height, num_colors)
bm2 = displayio.Bitmap(width, height, num_colors)
tg1 = displayio.TileGrid(bm1, pixel_shader=palette)
tg2 = displayio.TileGrid(bm2, pixel_shader=palette)
g1 = displayio.Group()
g1.append(tg1)
g2 = displayio.Group()
g2.append(tg2)
# display.show(g1)
self.width = width
self.height = height
self.num_colors = num_colors
self.curr = bm1
self.next = bm2
self.curr_group = g1
self.next_group = g2
def update(self, display):
width = self.width
height = self.height
num_colors = self.num_colors
curr = self.curr
next = self.next
# render to next framebuffer while current one is shown
t = time.monotonic()
# all i want for christmas is numpy...
# linear algebra heavy stuff is pretty obnoxious in plain python
# light direction
(lig_x, lig_y, lig_z) = normalize(0.6, 0.5, 0.4)
# rotate camera around the center over time
angle = -0.1*t
# angle = -.25*t
ro_x = 2.5 * cos(angle)
ro_y = 1.0
ro_z = 2.5 * sin(angle)
# camera matrix
#
# M = [ [ ww_x, uu_x, vv_x ]
# [ ww_y, uu_y, vv_y ]
# [ ww_z, uu_z, vv_z ] ]
ww_x = 0.0 - ro_x
ww_y = 1.0 - ro_y
ww_z = 0.0 - ro_z
(ww_x, ww_y, ww_z) = normalize(ww_x, ww_y, ww_z)
(uu_x, uu_y, uu_z) = cross(ww_x, ww_y, ww_z, 0.0, 1.0, 0.0)
(uu_x, uu_y, uu_z) = normalize(uu_x, uu_y, uu_z)
(vv_x, vv_y, vv_z) = cross(uu_x, uu_y, uu_z, ww_x, ww_y, ww_z)
(vv_x, vv_y, vv_z) = normalize(vv_x, vv_y, vv_z)
# sphere locations
(sph1_x, sph1_y, sph1_z) = (-1.2, 1.0, 0.0)
(sph2_x, sph2_y, sph2_z) = ( 1.0, 1.5, 0.0)
for y in range(height):
for x in range(width):
# normalized screen space
# p_y in -1 .. 1
# p_x in -2 .. 2
p_x = (2.0 * x - width) / height
p_y = -(2.0 * y - height) / height
# create view ray
# rd = normalize(M * [ p_x, p_y, 1.5 ])
# rd = normalize(p_x*uu + p_y*vv + 1.5*ww)
rd_x = (p_x*uu_x) + (p_y*vv_x) + (1.5*ww_x)
rd_y = (p_x*uu_y) + (p_y*vv_y) + (1.5*ww_y)
rd_z = (p_x*uu_z) + (p_y*vv_z) + (1.5*ww_z)
(rd_x, rd_y, rd_z) = normalize(rd_x, rd_y, rd_z)
# l = clamp(0.0, 1.0, rd_z)
# next[x, y] = round((num_colors - 1) * l)
# continue
# raytrace
tmin = 4.0
# rendered lightness. initial value is also bg color (no hit).
l = 0.0
# ray position
pos_x = 0.0
pos_y = 0.0
pos_z = 0.0
# normal vector
nor_x = 0.0
nor_y = 0.0
nor_z = 0.0
occ = 1.0
# raytrace sphere 1
h = i_sphere(ro_x, ro_y, ro_z, rd_x, rd_y, rd_z, sph1_x, sph1_y, sph1_z)
if h > 0.0 and h < tmin:
tmin = h
# pos = ro + h*rd
pos_x = ro_x + h * rd_x
pos_y = ro_y + h * rd_y
pos_z = ro_z + h * rd_z
# nor = normalize(pos - sph1)
nor_x = pos_x - sph1_x
nor_y = pos_y - sph1_y
nor_z = pos_z - sph1_z
(nor_x, nor_y, nor_z) = normalize(nor_x, nor_y, nor_z)
occ = 0.5 + 0.5*nor_y
occ *= occ_sphere(pos_x, pos_y, pos_z, nor_x, nor_y, nor_z, sph1_x, sph1_y, sph1_z)
# raytrace sphere 2
h = i_sphere(ro_x, ro_y, ro_z, rd_x, rd_y, rd_z, sph2_x, sph2_y, sph2_z)
if h > 0.0 and h < tmin:
tmin = h
# pos = ro + h*rd
pos_x = ro_x + h * rd_x
pos_y = ro_y + h * rd_y
pos_z = ro_z + h * rd_z
# nor = normalize(pos - sph2)
nor_x = pos_x - sph2_x
nor_y = pos_y - sph2_y
nor_z = pos_z - sph2_z
(nor_x, nor_y, nor_z) = normalize(nor_x, nor_y, nor_z)
occ = 0.5 + 0.5*nor_y
occ *= occ_sphere(pos_x, pos_y, pos_z, nor_x, nor_y, nor_z, sph2_x, sph2_y, sph2_z)
# shading + lighting
if tmin < 4.0:
# pos = ro + tmin*rd
pos_x = ro_x + tmin * rd_x
pos_y = ro_y + tmin * rd_y
pos_z = ro_z + tmin * rd_z
# shadows
sha = 1.0
# # skip first sphere since its shadow doesn't hit anything
# sha *= ss_sphere(pos_x, pos_y, pos_z, lig_x, lig_y, lig_z, sph1_x, sph1_y, sph1_z)
sha *= ss_sphere(pos_x, pos_y, pos_z, lig_x, lig_y, lig_z, sph2_x, sph2_y, sph2_z)
l = 0.0
# ambient occlusion
l += 0.5 * occ
# lambert shading
l += 5.0 * lambert(lig_x, lig_y, lig_z, nor_x, nor_y, nor_z) * sha
l *= 0.22
# gamma adjustment
l = clamp(0.0, 1.0, math.pow(l, 0.85))
next[x, y] = round((num_colors - 1) * l)
# swap frame buffers
self.curr = next
self.next = curr
curr_group = self.curr_group
self.curr_group = self.next_group
self.next_group = curr_group
# show new framebuffer
display.show(self.curr_group)
class Button:
def __init__(self, button_pin):
self.press_counter = countio.Counter(
pin=button_pin,
edge=countio.Edge.RISE,
pull=digitalio.Pull.UP,
)
self.prev_total = 0
# Returns the number of button presses since we last checked
def new_presses(self):
new_total = self.press_counter.count
new_presses = new_total - self.prev_total
self.prev_total = new_total
return new_presses
def main():
display = make_display()
screens = [
StartScreen,
CircleScreen,
WarpScreen,
Warp2Screen,
SpheresScreen,
GameOfLifeScreen,
# ColorTestScreen,
]
prev_screen_idx = -1
screen_idx = 0
up_button = Button(board.BUTTON_UP)
# down_button = Button(board.BUTTON_DOWN)
while True:
up_button_presses = up_button.new_presses()
if up_button_presses > 0:
screen_idx = (screen_idx + 1) % len(screens)
if screen_idx != prev_screen_idx:
prev_screen_idx = screen_idx
screen = screens[screen_idx](display)
screen.update(display)
try:
main()
except Exception as err:
print(f"Error: {err}")
(for reference) one-time setup process #
Board product page: https://www.adafruit.com/product/4745
Board info:
UF2 Bootloader v1.23.1-adafruit.1-328-gf06693a SFHWRO
Model: Matrix Portal M4
Board-ID: SAMD51J19A-MatrixPortal-v0
When first connected to power, we see the cool little falling sand demo that comes pre-installed.
After connecting it to a machine through the USB-C port, double-press the RST button on the board; the debug light flashes and a USB-drive called MATRIXBOOT
mounts on the connected machine.
If nothing happens, make sure the USB-C cable you're using supports data transfer and not just power.
The pre-installed contents of the drive:
$ ls /media/phlip9/MATRIXBOOT
CURRENT.UF2
INDEX.HTM
INFO_UF2.TXT
We then install the CircuitPython "OS" onto the board:
$ cd /tmp
$ wget https://downloads.circuitpython.org/bin/matrixportal_m4/en_US/adafruit-circuitpython-matrixportal_m4-en_US-7.3.3.uf2
$ cp adafruit-circuitpython-matrixportal_m4-en_US-7.3.3.uf2 /media/phlip9/MATRIXBOOT
The lights should flash a bit and the disk will unmount from your machine. A second later another USB disk mounts called CIRCUITPY
.
Download and install the Mu editor.
On linux I had to add my user, phlip9
, to the dialout
group and then restart for Mu+CircuitPython to work. macOS and Windows users can skip this step.
$ sudo adduser phlip9 dialout
Open the Mu editor and select "Mode", then choose "CircuitPython". If everything was setup properly, it should auto-detect the board.
Close any random "untitled" file it tries to create. Next hit the "Load" icon and navigate to the CIRCUITPY
drive. Choose the code.py
file. You should see something like:
print("Hello, World!")
Click the "Serial" button to show the device print
output. Save the currently open code.py
file and the board will automatically restart and run the file!