Skip to content

Instantly share code, notes, and snippets.

@wcarss
Last active March 9, 2021 00:44
Show Gist options
  • Save wcarss/a6ff897fb9c50ac34875dbd5d8599927 to your computer and use it in GitHub Desktop.
Save wcarss/a6ff897fb9c50ac34875dbd5d8599927 to your computer and use it in GitHub Desktop.
diffuse raymarcher from https://ch-st.de/its-ray-marching-march/ ported to python (tested in 2.7 and ~3.8.6)
# original C code from https://ch-st.de/its-ray-marching-march/
# retrieved, roughly ported, and refactored a little on 2021-03-08
from __future__ import print_function
import math
import time
import sys
class Vec3:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def length(self):
return math.sqrt(
self.x*self.x + self.y*self.y + self.z*self.z
)
def normalize(self):
l = self.length()
self.x = self.x / l
self.y = self.y / l
self.z = self.z / l
def __mul__(self, scale_factor):
new = Vec3(self.x, self.y, self.z)
new.x *= scale_factor
new.y *= scale_factor
new.z *= scale_factor
return new
def __add__(self, other):
new = Vec3(self.x, self.y, self.z)
new.x += other.x
new.y += other.y
new.z += other.z
return new
def __sub__(self, other):
new = Vec3(self.x, self.y, self.z)
new.x -= other.x
new.y -= other.y
new.z -= other.z
return new
def raymarch(t, framebuffer, width, height):
pixels = " .:+|0#"
for y in range(height):
for x in range(width):
pos = Vec3(0.0, 0.0, -3.0)
target = Vec3(
x / (1.0 * width) - 0.5,
(y / (1.0 * height) - 0.5) * (height / (1.0 * width)) * 1.5,
-1.5
)
ray = target - pos
ray.normalize()
pxl = pixels[0]
dist = 0
max = 9999.0
for i in range(15000):
if (abs(pos.x) > max or
abs(pos.y) > max or
abs(pos.z) > max):
break
dist = sdf(pos)
if dist < 1e-6:
pxl = shade(pos, t, pixels)
break
pos = pos + ray * dist
framebuffer[y * width + x] = pxl
def sdf(pos):
center = Vec3(0.0, 0.0, 0.0)
return (pos - center).length() - 0.2
def shade(pos, t, pixels):
L = Vec3(
50*math.sin(t),
20,
50*math.cos(t)
)
L.normalize()
dt = 1e-6
current_val = sdf(pos)
x = Vec3(pos.x + dt, pos.y, pos.z)
dx = sdf(x) - current_val
y = Vec3(pos.x, pos.y + dt, pos.z)
dy = sdf(y) - current_val
z = Vec3(pos.x, pos.y, pos.z + dt)
dz = sdf(z) - current_val
N = Vec3(0,0,0)
N.x = (dx - pos.x) / dt
N.y = (dy - pos.y) / dt
N.z = (dz - pos.z) / dt
if N.length() < 1e-9:
return pixels[0]
N.normalize()
diffuse = L.x * N.x + L.y * N.y + L.z * N.z
diffuse = (diffuse + 1.0) / 2.0 * len(pixels)
return pixels[int(math.floor(diffuse)) % len(pixels)]
def cls():
# Terminal clear sequence
cls_seq = u"\u001b[1;1H\u001b[2J"
sys.stdout.write(cls_seq)
sys.stdout.flush()
def printfb(framebuffer, width, height):
fb = 0
cls()
for y in range(height):
sys.stdout.write(''.join(framebuffer[fb:fb+width]))
sys.stdout.write('\n')
sys.stdout.flush()
fb += width
def main():
width = 80
height = 40
framebuffer = []
for i in range(width * height):
framebuffer.append('')
t = 0
while(True):
raymarch(t, framebuffer, width, height)
printfb(framebuffer, width, height)
# change these numbers as desired for spin+update speed
time.sleep(0.08)
t += 0.35
if __name__ == '__main__':
main()
@wcarss
Copy link
Author

wcarss commented Mar 9, 2021

below, I've also extended the original by adding some very hacky object representations, a more varied gradient representation, and implemented an awful background parallax effect, to make a silly space scene:

image

# original C code from https://ch-st.de/its-ray-marching-march/
# retrieved 2021-03-08 and roughly ported to python

from __future__ import print_function
import math
import time
import sys
import random

class Vec3:
  def __init__(self, x, y, z):
    self.x = x
    self.y = y
    self.z = z

  def length(self):
    return math.sqrt(
      self.x*self.x + self.y*self.y + self.z*self.z
    )

  def normalize(self):
    l = self.length()
    self.x = self.x / l
    self.y = self.y / l
    self.z = self.z / l

  def __mul__(self, scale_factor):
    new = Vec3(self.x, self.y, self.z)
    new.x *= scale_factor
    new.y *= scale_factor
    new.z *= scale_factor
    return new

  def __add__(self, other):
    new = Vec3(self.x, self.y, self.z)
    new.x += other.x
    new.y += other.y
    new.z += other.z
    return new

  def __sub__(self, other):
    new = Vec3(self.x, self.y, self.z)
    new.x -= other.x
    new.y -= other.y
    new.z -= other.z
    return new


class SceneObject:
  def __init__(self, x, y, z, size):
    self.pos = Vec3(x, y, z)
    self.size = size


def raymarch(t, objects, framebuffer, width, height):
  for y in range(height):
    for x in range(width):
      pos = Vec3(0.0, 0.0, -3.0)
      target = Vec3(
        x / float(width) - 0.5,
        (y / float(height) - 0.5) * ((float(height) / width) * 1.5),
        -1.5
      )

      ray = target - pos
      ray.normalize()
      val = 0
      dist = 0
      maximum = 9999.0

      for i in range(15000):
        if (abs(pos.x) > maximum or
          abs(pos.y) > maximum or
          abs(pos.z) > maximum):
          break

        for obj in objects:
          dist = sdf(pos, obj)
          broke = False
          if dist < 1e-6:
            val = shade(pos, t, obj)
            broke = True
            break

        if broke:
          break

        pos = pos + ray * dist

      framebuffer[y * width + x] = val


def sdf(pos, obj):
  return (pos - obj.pos).length() - obj.size


def shade(pos, t, obj):
  L = Vec3(
    90*math.sin(t),
    50,
    90*math.cos(t)
  )
  L.normalize()
    
  dt = 1e-6
  current_val = sdf(pos, obj)
  
  x = Vec3(pos.x + dt, pos.y, pos.z)
  dx = sdf(x, obj) - current_val
  
  y = Vec3(pos.x, pos.y + dt, pos.z)
  dy = sdf(y, obj) - current_val
  
  z = Vec3(pos.x, pos.y, pos.z + dt)
  dz = sdf(z, obj) - current_val

  N = Vec3(0, 0, 0)
  N.x = (dx - pos.x) / dt
  N.y = (dy - pos.y) / dt
  N.z = (dz - pos.z) / dt

  if N.length() < 1e-9:
    return 0
  
  N.normalize()

  diffuse = L.x * N.x + L.y * N.y + L.z * N.z
  diffuse = (diffuse + 1.0) / 2.0
  return diffuse


def cls():
  # Terminal clear sequence
  cls_seq = u"\u001b[1;1H\u001b[2J"
  sys.stdout.write(cls_seq)
  sys.stdout.flush()


def printfb(framebuffer, bgbuffer, width, height):
  fb = 0
  cls()
  #pixels = " .,;:!I0@#"
  pixels = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "[::-1]

  def light_to_pixel(value):
    value = int(math.floor(value * len(pixels)))
    if value <= 1:
      return pixels[value]

    r = random.random()
    if r < 0.25:
      value += 1
    elif r > 0.75:
      value -= 1
      value = max(value, 0)

    return pixels[value % len(pixels)]

  for y in range(height):
    for index in range(width):
      val = max(framebuffer[y*width+index], bgbuffer[y*width+index])
      sys.stdout.write(light_to_pixel(val))
    sys.stdout.write('\n')
    sys.stdout.flush()

def main():
  width = 140
  height = 44
  framebuffer = []
  bgbuffer = []
  for i in range(width * height):
    if random.random() > 0.97:
      bgbuffer.append(0.02)
    else:
      bgbuffer.append(0)
    framebuffer.append(0)

  objects = [
    SceneObject(0, -0.3, 0, 0.28),
    SceneObject(0, 0.5, 0.0, 0.45),
    SceneObject(0.7, 0.5, 0.2, 0.1)
  ]

  t = 0
  count = 0
  while(True):
    raymarch(t, objects, framebuffer, width, height)
    printfb(framebuffer, bgbuffer, width, height)
    if count > 8: 
      bgbuffer = bgbuffer[width:]+bgbuffer[0:width]
      count = 0
    # change these numbers as desired for spin+update speed
    time.sleep(0.1)
    t += 0.12
    count += 1


if __name__ == '__main__':
  main()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment