Package: play3d
3D Playground - on Python from scratch.TL;DR: Basic 3D world playground with animations andcameracompletely from scratch(only 2D pixels).This implementation / API only for demonstration andplaygroundpurposes based onPerspective projection.Can be used on top ofany2d graphics engine/lib(frame buffers, sdl and etc.)Not implemented features due to low performance:Face clipping not implemented, vertices clipping ignored tooFlat shading and Gouraud shading not implemented.Z-bufferingmodels.ModelAPI is open demonstration ofMVPmodel and is definitely a good starting point/topic for 3D graphics.Also you can plot any function on 3D scene.InstallHow to useModel View ProjectionProjectionCameraCamera scene exampleMesh and WireframeRasterization3D PlottingBasic Wavefront .obj format supportModel APITrajectory APIPygame ExampleInstallpip install play3dHow to useThere is only one requirement - to provide 2D pixel and line renderer(drawer)As current example usespygame,pip install pygame==2.0.1 # recommended version
# You have to install sdl lib separately
# for Mac OS:
brew install sdl2 sdl2_gfx sdl2_image sdl2_mixer sdl2_net sdl2_ttffromplay3d.three_dimportDeviceimportpygame# our adapter will rely on pygame rendererput_pixel=lambdax,y,color:pygame.draw.circle(screen,color,(x,y),1)# we certainly can draw lines ourselves using put_pixel three_d.drawline# but implementation below - much fasterline_adapter=lambdap1,p2,color:pygame.draw.line(screen,color,(p1[x],p1[y]),(p2[x],p2[y]),1)width,height=1024,768# should be same as 2D providerDevice.viewport(width,height)Device.set_renderer(put_pixel,line_adapter)screen=pygame.display.set_mode(Device.get_resolution())That's all we need for setting up environment.
Now we can create and render model objects by callingModel.draw()at each frame update (SeeExample)To create model you can simply pass 3D world vertices as 2-d listModel(data=data)It is possible to provide faces as 2d arrayModel(data=data, faces=faces). Face index starts from 1.  Only triangles supported. For more information see below.Simply by providing 3D (or 4D homogeneous where w=1)datavertices list - Model transforms this coordinates from 3D world space to projected screen spacefromplay3d.modelsimportModel# our 2D library renderer setup.. See above.# Cube model. Already built-in `models.Cube`cube=Model(position=(0,0,0),data=[[-1,1,1,1],[1,1,1,1],[-1,-1,1,1],[1,-1,1,1],[-1,1,-1,1],[1,1,-1,1],[1,-1,-1,1],[-1,-1,-1,1]])whileTrue:# your render lib/methodcube.draw()Model View Projectionmodels.Modelandthree_d.Cameraimplements all MVP(SeeModel.draw).ProjectionHere we use perspective projection matrixZ axis of clipped cube(from frustum) mapped to [-1, 1] and our camera directed to -z axis (OpenGL convention)Projection Matrix can be tuned there (aspect ratio, FOV and etc.) \Camera.near=1Camera.far=10Camera.fov=60Camera.aspect_ratio=3/4World cameraBy OpenGL standard we basically move our scene.
Facing direction considered when we move our camera in case of rotations(direction vector will be transformed too)Camera can be moved throughthree_d.CameraAPI:fromplay3d.three_dimportCameracamera=Camera.get_instance()# move camera to x, y, z with 0.5 step considering facing directioncamera['x']+=0.5camera['y']+=0.5camera['z']+=0.5camera.move(0.5,0.5,0.5)# identical above# rotate camera to our left on XZ planecamera.rotate('y',2)#Camera keys exampleMesh and WireframeTo exploit mesh one should provide bothdataandfaces. Face represents triple group of vertices index referenced fromdata. Face index starts from 1.By default object rendered as wireframefromplay3d.modelsimportModeltriangle=Model(position=(-5,3,-4),data=[[-3,1,-7,1],[-2,2,-7,1],[-1,0,-7,1],],faces=[[1,2,3]])RasterizationBy default if data and faces provided, rasterization will be enabled.For rasterization we use - standard slope algorithm with horizontal filling lines.fromplay3d.modelsimportModelwhite=(230,230,230)suzanne=Model.load_OBJ('suzanne.obj.txt',position=(-4,2,-6),color=white,rasterize=True)suzanne_wireframe=Model.load_OBJ('suzanne.obj.txt',position=(-4,2,-6),color=white)suzanne.rotate(0,-14)suzanne_wireframe.rotate(0,14)3D plottingYou can plot any function you want by providing parametric equation asfunc(*parameters) -> [x, y, z].
For example, sphere and some awesome wave both polar and parametric equations(Sphere built-in asModels.Sphere):importmathfromplay3d.modelsimportPlotdeffn(phi,theta):return[math.sin(phi*math.pi/180)*math.cos(theta*math.pi/180),math.sin(theta*math.pi/180)*math.sin(phi*math.pi/180),math.cos(phi*math.pi/180)]sphere_model=Plot(func=fn,allrange=[0,360],position=(-4,2,1),color=(0,64,255))blow_your_head=Plot(position=(-4,2,1),color=(0,64,255),func=lambdax,t:[x,math.cos(x)*math.cos(t),math.cos(t)],allrange=[0,2*math.pi],interpolate=75)OBJ formatWawefront format is widely used as a standard in 3D graphicsYou can import your model here. Only vertices and faces supported.Model.load_OBJ(cls, path_or_url, wireframe=False, **all_model_kwargs)You can find examples heregithub.com/alecjacobson/common-3d-test-modelsYou might have to normalize(scale and etc.)each.objsample differentlyOnly vertices and faces are supported.Model.load_OBJ('beetle.obj.txt',wireframe=True,color=white,position=(-2,2,-4),scale=3)Models APIModels.ModelFieldsDescriptionpositiontuple=(0, 0, 0)with x, y, z world coordinatesscaleinteger(=1)colortuple(255, 255, 255)datalist[[x, y, z, [w=1]]]- Model vertices(points)faceslist[[A, B, C]]- Defines triangles See:Mesh and Wireframerasterizebool(=True)- Rasterize - "fill" an objectshimmeringbool(=False)- color flickering/dancing# Initial Model Matrixmodel.matrix=Matrix([[1*scale,0,0,0],[0,1*scale,0,0],[0,0,1*scale,0],[*position,1]])Methodsmodel_obj @ translate(x, y, z)translates object's model matrix (in world space)Trajectory APIModels.TrajectoryFieldsDescriptionfuncfuncParametrized math function which takes*argsand returns world respective coordinatestuple=(x, y, z)To move our object through defined path we can build Trajectory for our object.
You can provide any parametric equation with args.World coordinates defined byfunc(*args)tuple output.Methodsrotate(self, angle_x, angle_y=0, angle_z=0)Rotates object relative to particular axis plane. First object translated from the world space back to local origin, then we rotate the objectroute(self, trajectory: 'Trajectory', enable_trace=False)Set the function-based  trajectory routing for the object.trajectoryTrajectory- trajectory stateenable_tracebool- Keep track of i.e. draw trajectory path (breadcrumbs)Exampleimportmathfromplay3d.modelsimportSphere,Trajectorywhite=(230,230,230)moving_sphere=Sphere(position=(1,3,-5),color=white,interpolate=50)moving_sphere.route(Trajectory.ToAxis.Z(speed=0.02).backwards())whirling_sphere=Sphere(position=(1,3,-5),color=white,interpolate=50)# Already built-in as Trajectory.SineXY(speed=0.1)whirling_sphere.route(Trajectory(lambdax:[x,math.sin(x)],speed=0.1))whileTrue:# inside your "render()"moving_sphere.draw()whirling_sphere.draw()Pygame exampleimportloggingimportsysimportpygamefromplay3d.modelsimportModel,Gridfrompygame_utilsimporthandle_camera_with_keys# custom keyboard handling - moving camerafromplay3d.three_dimportDevice,Camerafromplay3d.utilsimportcapture_fpslogging.basicConfig(stream=sys.stdout,level=logging.INFO)black,white=(20,20,20),(230,230,230)Device.viewport(1024,768)pygame.init()screen=pygame.display.set_mode(Device.get_resolution())# just for simplicity - array access, we should avoid thatx,y,z=0,1,2# pygame sdl line is faster than default oneline_adapter=lambdap1,p2,color:pygame.draw.line(screen,color,(p1[x],p1[y]),(p2[x],p2[y]),1)put_pixel=lambdax,y,color:pygame.draw.circle(screen,color,(x,y),1)Device.set_renderer(put_pixel,line_renderer=line_adapter)grid=Grid(color=(30,140,200),dimensions=(30,30))# be aware of different scaling of .obj samples. Only vertices and faces supported!suzanne=Model.load_OBJ('https://raw.githubusercontent.com/OpenGLInsights/OpenGLInsightsCode/master/Chapter%2026%20Indexing%20Multiple%20Vertex%20Arrays/article/suzanne.obj',position=(3,2,-7),color=white,rasterize=True)beetle=Model.load_OBJ('https://raw.githubusercontent.com/alecjacobson/common-3d-test-models/master/data/beetle.obj',wireframe=False,color=white,position=(0,2,-11),scale=3)beetle.rotate(0,45,50)camera=Camera.get_instance()# move our camera up and back a bit, from origincamera.move(y=1,z=2)@capture_fpsdefframe():ifpygame.event.get(pygame.QUIT):sys.exit(0)screen.fill(black)handle_camera_with_keys()# to move our camera - walk, can be ignoredgrid.draw()beetle.draw()suzanne.rotate(0,1,0).draw()pygame.display.flip()whileTrue:frame()