Using without Pygame¶
The following code will load a 3D mesh, render it, and save it to a file, using Pillow instead of Pygame.
from PIL import Image
from ntracer import NTracer,ImageFormat,Channel,BlockingRenderer
from ntracer import wavefront_obj
width = 640
height = 480
format = ImageFormat(width,height,[
Channel(8,1,0,0),
Channel(8,0,1,0),
Channel(8,0,0,1),
Channel(8,0,0,0)])
nt = NTracer(3)
data = wavefront_obj.load_obj('monkey.obj')
scene = nt.build_composite_scene(data)
camera = nt.Camera()
camera.translate(nt.Vector.axis(2,-5))
scene.set_camera(camera)
render = BlockingRenderer()
buffer = bytearray(width * height * format.bytes_per_pixel)
render.render(buffer,format,scene)
mode = 'RGBX'
im = Image.frombuffer(mode,(width,height),buffer,'raw',mode,0,1)
im.convert('RGB').save('monkey.png')
Here is a breakdown of the code:
width = 640
height = 480
format = ImageFormat(width,height,[
Channel(8,1,0,0),
Channel(8,0,1,0),
Channel(8,0,0,1),
Channel(8,0,0,0)])
Since Pygame is not being used, the image format needs to be specified manually,
using render.ImageFormat
.
Here we specify that the image will be 640 by 480 pixels, with each pixel
containing 8 bits for the red component (1×R + 0×G + 0×B
) followed by 8 bits
for the green component (0×R + 1×G + 0×B
) followed by 8 bits for the blue
component (0×R + 0×G + 1×B
) followed by 8 bits of padding. The reason for
the padding will be explained later.
nt = NTracer(3)
data = wavefront_obj.load_obj('monkey.obj')
scene = nt.build_composite_scene(data)
camera = nt.Camera()
camera.translate(nt.Vector.axis(2,-5))
scene.set_camera(camera)
For an explanation of this code, see Basic Usage.
render = BlockingRenderer()
The two main renderers are render.BlockingRenderer
and
render.CallbackRenderer
(pygame_render.PygameRenderer
derives from render.CallbackRenderer
). While both renderers can use
any number of threads, only render.CallbackRenderer
works
asynchronously, allowing the thread that started the render to continue while
the renderer is busy. render.BlockingRenderer
will use the invoking
thread as one of its worker threads and won’t return until renderering is
finished.
Since the main thread doesn’t need to do anything else in this example, we use
render.BlockingRenderer
.
buffer = bytearray(width * height * format.bytes_per_pixel)
render.render(buffer,format,scene)
We create a bytearray
big enough to hold the image data and render the scene
onto it. There is nothing special about the bytearray
class. Any object that
can be written to and supports the buffer interface can be drawn onto.
mode = 'RGBX'
im = Image.frombuffer(mode,(width,height),buffer,'raw',mode,0,1)
We then use frombuffer
to turn the array into an instance of Image
. There are other ways to load
image data into an Image
class but this is the most efficient because this
way the data is used directly instead of being copied. This also means that to
update the Image
object, all we have to do is draw onto the same
bytearray
object again.
Only certain formats can be used by frombuffer
without copying, which is why
we use RGBX
and not just RGB
and consequently why we needed padding in
our pixel format above.
In case you’re wondering what the last two number passed to frombuffer
mean:
the zero means there is no padding between lines and the one means the lines of
the image are stored from top to bottom (a value of -1 would indicate lines
being stored bottom to top).
im.convert('RGB').save('monkey.png')
Finally we save the image to a PNG file. To do so, we have to first convert the
image to RGB
.