Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: jtauber/minilight
base: 33b4a2ccb4
...
head fork: jtauber/minilight
compare: 2cf399d151
Checking mergeability… Don't worry, you can still create the pull request.
  • 14 commits
  • 10 files changed
  • 0 commit comments
  • 1 contributor
View
2  LICENSE.txt
@@ -1,5 +1,5 @@
Copyright (c) 2007-2008, Harrison Ainsworth / HXA7241 and Juraj Sukop.
-Copyright (c) 2009, James Tauber.
+Copyright (c) 2009-2011, James Tauber.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
View
4 README
@@ -12,7 +12,7 @@ it better and possibly add new features but I have other goals such as:
8th power "Mandelbulb" in particular
- removing features and adding them back in incrementally to produce a
tutorial
- - exploring how to optimize with things like psyco, numpy, partial C
+ - exploring how to optimize with things like PyPy, psyco, numpy, partial C
implementation, etc.
See branches 'no-spatialindex', 'no-autosave', and 'simple-mainline' for
@@ -24,4 +24,4 @@ historical. You have been warned.
James Tauber
jtauber@jtauber.com
-November 2009
+November 2009 and June 2011
View
8 camera.py
@@ -3,11 +3,9 @@
# Copyright (c) 2007-2008, Harrison Ainsworth / HXA7241 and Juraj Sukop.
# http://www.hxa7241.org/
#
-# Copyright (c) 2009, James Tauber.
+# Copyright (c) 2009-2011, James Tauber.
-from numpy import array
-
from math import pi, tan
from random import random
from raytracer import RayTracer
@@ -64,8 +62,8 @@ def get_frame(self, scene, image):
# calculate radiance from that direction
- radiance = array(list(raytracer.get_radiance(self.view_position, sample_direction)))
+ radiance = raytracer.get_radiance(self.view_position, sample_direction)
# and add to image
- image.add_to_pixel(x, y, radiance)
+ image.add_radiance(x, y, radiance)
View
120 image.py
@@ -3,59 +3,107 @@
# Copyright (c) 2007-2008, Harrison Ainsworth / HXA7241 and Juraj Sukop.
# http://www.hxa7241.org/
#
-# Copyright (c) 2009, James Tauber.
+# Copyright (c) 2009-2011, James Tauber.
+from array import array
from math import log10
-from numpy import zeros, array
-PPM_ID = 'P6'
-MINILIGHT_URI = 'http://www.hxa7241.org/minilight/'
+
+PPM_ID = "P6"
+MINILIGHT_URI = "http://www.hxa7241.org/minilight/"
+
+# how much each channel contributes to luminance
+RGB_LUMINANCE = (0.2126, 0.7152, 0.0722)
+
DISPLAY_LUMINANCE_MAX = 200.0
-RGB_LUMINANCE = array([0.2126, 0.7152, 0.0722])
+
+# formula from Ward "A Contrast-Based Scalefactor for Luminance Display"
+SCALEFACTOR_NUMERATOR = 1.219 + (DISPLAY_LUMINANCE_MAX * 0.25) ** 0.4
+
+
GAMMA_ENCODE = 0.45
class Image(object):
def __init__(self, width, height):
+ """
+ initialize blank image.
+ """
self.width = width
self.height = height
- self.pixels = zeros((width * height, 3))
+ self.data = array("d", [0]) * (width * height * 3)
+
+ def _index(self, t):
+ x, y, channel = t
+ index = (x + ((self.height - 1 - y) * self.width)) * 3 + channel
+
+ return min(max(index, 0), len(self.data) - 1)
+
+ def __getitem__(self, t):
+ return self.data[self._index(t)]
+
+ def __setitem__(self, t, val):
+ self.data[self._index(t)] = val
- def add_to_pixel(self, x, y, radiance):
- if x >= 0 and x < self.width and y >= 0 and y < self.height:
- index = x + ((self.height - 1 - y) * self.width)
- self.pixels[index] += radiance
+ def add_radiance(self, x, y, radiance):
+ """
+ add radiance (an RGB tuple) to given x, y position on image.
+ """
+ self[x, y, 0] += radiance[0]
+ self[x, y, 1] += radiance[1]
+ self[x, y, 2] += radiance[2]
- def write_ppm(self, out, iteration):
+ def calculate_scalefactor(self, iterations):
+ """
+ calculate the linear tone-mapping scalefactor for this image assuming
+ the given number of iterations.
+ """
+ ## calculate the log-mean luminance of the image
- out.write('%s\n# %s\n\n%u %u\n255\n' % (PPM_ID, MINILIGHT_URI, self.width, self.height))
+ sum_of_logs = 0.0
- divider = 1.0 / (max(iteration, 0) + 1)
- tonemap_scaling = calculate_tone_mapping(self.pixels, divider)
+ for x in range(self.width):
+ for y in range(self.height):
+ lum = self[x, y, 0] * RGB_LUMINANCE[0]
+ lum += self[x, y, 1] * RGB_LUMINANCE[1]
+ lum += self[x, y, 2] * RGB_LUMINANCE[2]
+ lum /= iterations
+
+ sum_of_logs += log10(max(lum, 0.0001))
+
+ log_mean_luminance = 10.0 ** (sum_of_logs / (self.height * self.width))
- for pixel in self.pixels:
- for j in range(3):
- channel = pixel[j]
- gammaed = max(channel * divider * tonemap_scaling, 0.0) ** GAMMA_ENCODE
- out.write(chr(min(int((gammaed * 255.0) + 0.5), 255)))
-
- def save(self, filename, iteration):
- f = open(filename, 'wb')
- self.write_ppm(f, iteration)
- f.close()
-
-
-def calculate_tone_mapping(pixels, divider):
- sum_of_logs = 0.0
-
- for pixel in pixels:
- y = sum(divider * pixel * RGB_LUMINANCE) # pixel and RGB_LUMINANCE are vectors so this is a dot product
- sum_of_logs += log10(max(y, 0.0001))
+ ## calculate the scalefactor for linear tone-mapping
+
+ # formula from Ward "A Contrast-Based Scalefactor for Luminance Display"
+
+ scalefactor = (
+ (SCALEFACTOR_NUMERATOR / (1.219 + log_mean_luminance ** 0.4)) ** 2.5
+ ) / DISPLAY_LUMINANCE_MAX
+
+ return scalefactor
- log_mean_luminance = 10.0 ** (sum_of_logs / len(pixels))
- a = 1.219 + (DISPLAY_LUMINANCE_MAX * 0.25) ** 0.4
- b = 1.219 + log_mean_luminance ** 0.4
+ def display_pixels(self, iterations):
+ """
+ iterate over each channel of each pixel in image returning
+ gamma-corrected number scaled 0 - 1 (although not clipped to 1).
+ """
+ scalefactor = self.calculate_scalefactor(iterations)
+
+ for value in self.data:
+ yield max(value * scalefactor / iterations, 0) ** GAMMA_ENCODE
- return ((a / b) ** 2.5) / DISPLAY_LUMINANCE_MAX
+ def save(self, filename, iterations):
+ """
+ save the image to given filename assuming the given number
+ of iterations.
+ """
+
+ with open(filename, "wb") as f:
+ f.write("%s\n# %s\n\n%u %u\n255\n" % (
+ PPM_ID, MINILIGHT_URI, self.width, self.height))
+
+ for c in self.display_pixels(iterations):
+ f.write(chr(min(int((c * 255.0) + 0.5), 255)))
View
57 minilight-python.readme.txt
@@ -1,57 +0,0 @@
-
-
-MiniLight 1.5.2 Python
-======================================================================
-
-
-Copyright (c) 2007-2008, Harrison Ainsworth / HXA7241 and Juraj Sukop.
-http://wwww.hxa7241.org/minilight/
-
-2008-02-17
-
-
-
-
-Contents
---------
-
-* Description
-* Usage
-* Acknowledgements
-
-
-
-
-Description
------------
-
-This Python translation of MiniLight is a guest contribution by Juraj Sukop.
-
-MiniLight is a minimal global illumination renderer. See the main MiniLight
-readme for a general description.
-
-
-
-
-Usage
------
-
-Requirements:
-* Python 2.5
-* Psyco 1.6 is optional: if using those first two lines in minilight.py
-
-Installation:
-1. extract/copy the files to somewhere
-
-
-
-
-Acknowledgements
-----------------
-
-### tools ###
-
-* Python 2.5.1
- http://www.python.org/
-* Psyco 1.6
- http://psyco.sourceforge.net/
View
138 minilight.py
@@ -6,114 +6,42 @@
# Copyright (c) 2009, James Tauber.
-#import psyco
-#psyco.full()
-
from camera import Camera
from image import Image
from scene import Scene
-from parser import *
-
-from math import log10
-from sys import argv, stdout
-from time import time
-
-BANNER = """
- MiniLight 1.5.2 Python
- Copyright (c) 2008, Harrison Ainsworth / HXA7241 and Juraj Sukop.
- http://www.hxa7241.org/minilight/
- Copyright (c) 2009, James Tauber.
-"""
-
-HELP = """
-----------------------------------------------------------------------
- MiniLight 1.5.2 Python
-
- Copyright (c) 2008, Harrison Ainsworth / HXA7241 and Juraj Sukop.
- http://www.hxa7241.org/minilight/
- Copyright (c) 2009, James Tauber.
-
- 2009-11-21
-----------------------------------------------------------------------
-
-MiniLight is a minimal global illumination renderer.
-
-usage:
- minilight image_file_pathname
-
-The model text file format is:
- #MiniLight
-
- iterations
-
- imagewidth imageheight
-
- viewposition viewdirection viewangle
-
- skyemission groundreflection
- vertex0 vertex1 vertex2 reflectivity emitivity
- vertex0 vertex1 vertex2 reflectivity emitivity
- ...
-
--- where iterations and image values are ints, viewangle is a float,
-and all other values are three parenthised floats. The file must end
-with a newline. Eg.:
- #MiniLight
-
- 100
-
- 200 150
-
- (0 0.75 -2) (0 0 1) 45
-
- (3626 5572 5802) (0.1 0.09 0.07)
- (0 0 0) (0 1 0) (1 1 0) (0.7 0.7 0.7) (0 0 0)
-"""
+from scene_parser import *
+
+from sys import argv
+
MODEL_FORMAT_ID = "#MiniLight"
-SAVE_PERIOD = 180
-
-
-if __name__ == "__main__":
- if len(argv) < 2 or argv[1] == "-?" or argv[1] == "--help":
- print HELP
- else:
- print BANNER
-
- model_file_pathname = argv[1]
- image_file_pathname = model_file_pathname + ".ppm"
- model_file = open(model_file_pathname, "r")
-
- if model_file.next().strip() != MODEL_FORMAT_ID:
- raise "invalid model file"
-
- iterations = parse_iterations(model_file)
-
- width, height = parse_image_dimensions(model_file)
- image = Image(width, height)
-
- position, direction, angle = parse_camera_description(model_file)
- camera = Camera(position, direction, angle)
-
- sky_emission, ground_reflection = parse_sky_ground(model_file)
- triangles = parse_triangles(model_file)
- scene = Scene(sky_emission, ground_reflection, camera.view_position, triangles)
-
- model_file.close()
-
- last_time = time() - (SAVE_PERIOD + 1)
-
- try:
- for frame_no in range(1, iterations + 1):
- camera.get_frame(scene, image)
- if SAVE_PERIOD < time() - last_time or frame_no == iterations:
- last_time = time()
- image.save(image_file_pathname, frame_no - 1)
- stdout.write("\b" * ((int(log10(frame_no - 1)) if frame_no > 1 else -1) + 12) + "iteration: %u" % frame_no)
- stdout.flush()
- print "\nfinished"
- except KeyboardInterrupt:
- print "\ninterupted; saving..."
- image.save(image_file_pathname, frame_no)
- print "saved"
+
+
+model_file_pathname = argv[1]
+image_file_pathname = model_file_pathname + ".ppm"
+model_file = open(model_file_pathname, "r")
+
+if model_file.next().strip() != MODEL_FORMAT_ID:
+ raise "invalid model file"
+
+iterations = parse_iterations(model_file)
+
+width, height = parse_image_dimensions(model_file)
+image = Image(width, height)
+
+position, direction, angle = parse_camera_description(model_file)
+camera = Camera(position, direction, angle)
+
+sky_emission, ground_reflection = parse_sky_ground(model_file)
+triangles = parse_triangles(model_file)
+scene = Scene(sky_emission, ground_reflection, camera.view_position, triangles)
+
+model_file.close()
+
+for iteration in range(iterations):
+ camera.get_frame(scene, image)
+ print "iteration:", iteration + 1
+
+image.save(image_file_pathname, iteration + 1)
+print "\nfinished"
View
9 scene.py
@@ -7,7 +7,8 @@
from random import choice
-from spatialindex import SpatialIndex
+
+from spatialindex import SpatialIndex, NullSpatialIndex
from triangle import Triangle
from vector3f import Vector3f, ZERO, ONE, MAX
@@ -29,12 +30,10 @@ def __init__(self, sky_emission, ground_reflection, eye_position, t):
print "loaded %d triangles (%d emitters)" % (len(triangles), len(self.emitters))
- self.index = SpatialIndex(eye_position, triangles)
-
+ self.index = NullSpatialIndex(eye_position, triangles)
print "built spatial index (%d deep)" % self.index.deepest_level
-
self.get_intersection = self.index.get_intersection
-
+
def get_emitter(self):
if self.emitters:
emitter = choice(self.emitters)
View
0  parser.py → scene_parser.py
File renamed without changes
View
24 spatialindex.py
@@ -15,6 +15,30 @@
MAX_FLOAT = float(2**1024 - 2**971)
+
+class NullSpatialIndex(object):
+
+ def __init__(self, eye_position, items):
+ self.triangles = items
+ self.deepest_level = 0
+
+ def get_intersection(self, ray_origin, ray_direction, last_hit):
+ hit_object = hit_position = None
+
+ nearest_distance = MAX_FLOAT
+
+ for triangle in self.triangles:
+ distance = triangle.get_intersection(ray_origin, ray_direction)
+
+ if distance and (distance < nearest_distance):
+ hit_object = triangle
+ hit_position = ray_origin + ray_direction * distance
+ nearest_distance = distance
+
+ return hit_object, hit_position
+
+
+
class SpatialIndex(object):
def __init__(self, eye_position, items):
View
32 vector3f.py
@@ -2,15 +2,18 @@
#
# Copyright (c) 2007-2008, Harrison Ainsworth / HXA7241 and Juraj Sukop.
# http://www.hxa7241.org/
+#
+# Copyright (c) 2009-2011, James Tauber.
from math import sqrt
-class Vector3f(object):
+class Vector3f(object):
+
def __init__(self, *args):
- if len(args) == 1 and type(args[0]) == type(''):
- self.x, self.y, self.z = map(float, args[0].lstrip(' (').rstrip(') ').split())
+ if len(args) == 1 and type(args[0]) == type(""):
+ self.x, self.y, self.z = map(float, args[0].lstrip(" (").rstrip(") ").split())
elif type(args[0]) == Vector3f:
self.x, self.y, self.z = args[0].x, args[0].y, args[0].z
else:
@@ -19,12 +22,12 @@ def __init__(self, *args):
self.x = self.y = self.z = float(args[0])
if len(args) > 2:
self.y, self.z = float(args[1]), float(args[2])
-
+
def __iter__(self):
yield self.x
yield self.y
yield self.z
-
+
def __getitem__(self, key):
if key == 2:
return self.z
@@ -32,43 +35,44 @@ def __getitem__(self, key):
return self.y
else:
return self.x
-
+
def __neg__(self):
return Vector3f(-self.x, -self.y, -self.z)
-
+
def __add__(self, other):
return Vector3f(self.x + other.x, self.y + other.y, self.z + other.z)
-
+
def __sub__(self, other):
return Vector3f(self.x - other.x, self.y - other.y, self.z - other.z)
-
+
def __mul__(self, other):
if type(other) == Vector3f:
return Vector3f(self.x * other.x, self.y * other.y, self.z * other.z)
else:
return Vector3f(self.x * other, self.y * other, self.z * other)
-
+
def is_zero(self):
return self.x == 0.0 and self.y == 0.0 and self.z == 0.0
-
+
def dot(self, other):
return (self.x * other.x) + (self.y * other.y) + (self.z * other.z)
-
+
def unitize(self):
length = sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
one_over_length = 1.0 / length if length != 0.0 else 0.0
return Vector3f(self.x * one_over_length, self.y * one_over_length, self.z * one_over_length)
-
+
def cross(self, other):
return Vector3f((self.y * other.z) - (self.z * other.y),
(self.z * other.x) - (self.x * other.z),
(self.x * other.y) - (self.y * other.x))
-
+
def clamped(self, lo, hi):
return Vector3f(min(max(self.x, lo.x), hi.x),
min(max(self.y, lo.y), hi.y),
min(max(self.z, lo.z), hi.z))
+
ZERO = Vector3f(0.0)
ONE = Vector3f(1.0)
MAX = Vector3f(float(2**1024 - 2**971))

No commit comments for this range

Something went wrong with that request. Please try again.