Skip to content

Commit 9986ef3

Browse files
author
Tamerlan Abilov
committed
add source & README.md
1 parent 44d4c22 commit 9986ef3

File tree

17 files changed

+8541
-2
lines changed

17 files changed

+8541
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,4 @@ dmypy.json
127127

128128
# Pyre type checker
129129
.pyre/
130+
.idea

README.md

Lines changed: 325 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,325 @@
1-
# python-3d-from-zero
2-
Demonstration of building 3d world, 2d projection from scratch
1+
3D Playground - on Python from scratch.
2+
=====================================
3+
4+
![Pygame integration example](example/sm-example.gif)
5+
6+
7+
#### TL;DR: Some basic 3D world playground with animations and [camera](#camera-keys-example) completely from scratch(only 2D pixels).
8+
This implementation / API only for demonstration and *playground* purposes based on [Perspective projection](https://en.wikipedia.org/wiki/3D_projection#Perspective_projection).
9+
Can be used on top of **any** 2d graphics engine/lib(frame buffers, sdl and etc.)
10+
11+
Not implemented features due to low performance:
12+
* Face clipping not implemented, vertices clipping ignored too
13+
* Flat shading and Gouraud shading not implemented.
14+
* Z-buffering
15+
16+
`models.Model` API is open demonstration of [MVP](https://stackoverflow.com/questions/5550620/the-purpose-of-model-view-projection-matrix) model and is definitely a good starting point/topic for 3D graphics.
17+
18+
Also you can plot any function on 3D scene.
19+
20+
* [How to use](#how-to-use)
21+
* [Model View Projection](#model-view-projection)
22+
* [Projection](#projection)
23+
* [Camera](#world-camera)
24+
* [Camera scene example](#camera-keys-example)
25+
* [Mesh and Wireframe](#mesh-and-wireframe)
26+
* [Rasterization](#rasterization)
27+
* [3D Plotting](#3d-plotting)
28+
* [Basic Wavefront .obj format support](#obj-format)
29+
* [Model API](#models-api)
30+
* [Trajectory API](#trajectory-api)
31+
* [Pygame Example](#pygame-example)
32+
33+
## How to use
34+
35+
There is only one requirement - to provide 2D pixel and line renderer(drawer)
36+
37+
As current example uses `pygame`:
38+
```python
39+
from play3d.three_d import Device
40+
import pygame
41+
42+
# our adapter will rely on pygame renderer
43+
put_pixel = lambda x, y, color: pygame.draw.circle(screen, color, (x, y), 1)
44+
# we certainly can draw lines ourselves using put_pixel three_d.drawline
45+
# but implementation below - much faster
46+
line_adapter = lambda p1, p2, color: pygame.draw.line(screen, color, (p1[x], p1[y]), (p2[x], p2[y]), 1)
47+
48+
width, height = 1024, 768 # should be same as 2D provider
49+
Device.viewport(width, height)
50+
Device.set_renderer(put_pixel, line_adapter)
51+
screen = pygame.display.set_mode(Device.get_resolution())
52+
53+
```
54+
55+
That's all we need for setting up environment.
56+
Now we can create and render model objects by calling `Model.draw()` at each frame update (See example)\
57+
To create model you can simply pass 3D world vertices as 2-d list `Model(data=data)`
58+
59+
It is possible to provide faces as 2d array `Model(data=data, faces=faces)`. Face index starts from 1. Only triangles supported. For more information see below.
60+
61+
Simply by providing 3D (or 4D homogeneous where w=1) `data` vertices list - Model transforms this coordinates from 3D world space to projected screen space
62+
```python
63+
from play3d.models import Model
64+
65+
# our 2D library renderer setup.. See above.
66+
67+
# Cube model. Already built-in `models.Cube`
68+
cube = Model(position=(0, 0, 0),
69+
data=[
70+
[-1, 1, 1, 1],
71+
[1, 1, 1, 1],
72+
[-1, -1, 1, 1],
73+
[1, -1, 1, 1],
74+
[-1, 1, -1, 1],
75+
[1, 1, -1, 1],
76+
[1, -1, -1, 1],
77+
[-1, -1, -1, 1]
78+
])
79+
while True: # your render lib/method
80+
cube.draw()
81+
```
82+
## Model View Projection
83+
84+
`models.Model` and `three_d.Camera` implements all MVP(See `Model.draw`).
85+
86+
### Projection
87+
88+
Here we use perspective projection matrix\
89+
Z axis of clipped cube(from frustum) mapped to [-1, 1] and our camera directed to -z axis (OpenGL convention)\
90+
Projection Matrix can be tuned there (aspect ratio, FOV and etc.) \
91+
`Camera.near = 1`\
92+
`Camera.far = 10`\
93+
`Camera.fov = 60`\
94+
`Camera.aspect_ratio = 3/4`
95+
96+
### World camera
97+
98+
By OpenGL standard we basically move our scene.
99+
Facing direction considered when we move our camera in case of rotations(direction vector will be transformed too)\
100+
Camera can be moved through `three_d.Camera` API:
101+
```python
102+
from play3d.three_d import Camera
103+
camera = Camera.get_instance()
104+
105+
# move camera to x, y, z with 0.5 step considering facing direction
106+
camera['x'] += 0.5
107+
camera['y'] += 0.5
108+
camera['z'] += 0.5
109+
110+
camera.move(0.5, 0.5, 0.5) # identical above
111+
112+
# rotate camera to our left on XZ plane
113+
camera.rotate('y', 2) #
114+
```
115+
116+
#### Camera keys example
117+
![Pygame integration example](example/move-around.gif)
118+
119+
## Mesh and Wireframe
120+
121+
To exploit mesh one should provide both `data` and `faces`. Face represents triple group of vertices index referenced from `data`. Face index starts from 1.\
122+
By default object rendered as wireframe
123+
```python
124+
125+
from play3d.models import Model
126+
triangle = Model(position=(-5, 3, -4),
127+
data=[
128+
[-3, 1, -7, 1],
129+
[-2, 2, -7, 1],
130+
[-1, 0, -7, 1],
131+
], faces=[[1, 2, 3]])
132+
```
133+
134+
![Triangle wireframe](https://i.imgur.com/A7ktUd7.png)
135+
136+
137+
## Rasterization
138+
139+
By default if data and faces provided, rasterization will be enabled.\
140+
For rasterization we use - standard slope algorithm with horizontal filling lines.
141+
```python
142+
from play3d.models import Model
143+
144+
white = (230, 230, 230)
145+
suzanne = Model.load_OBJ('suzanne.obj.txt', position=(-4, 2, -6), color=white, rasterize=True)
146+
suzanne_wireframe = Model.load_OBJ('suzanne.obj.txt', position=(-4, 2, -6), color=white)
147+
suzanne.rotate(0, -14)
148+
suzanne_wireframe.rotate(0, 14)
149+
```
150+
151+
![Suzanne wireframe and rasterized](https://i.imgur.com/1vVlLt9.png)
152+
153+
## 3D plotting
154+
155+
You can plot any function you want by providing parametric equation as `func(*parameters) -> [x, y, z]`.
156+
For example, sphere and some awesome wave both polar and parametric equations(Sphere built-in as `Models.Sphere`):
157+
```python
158+
import math
159+
from play3d.models import Plot
160+
161+
def fn(phi, theta):
162+
163+
return [
164+
math.sin(phi * math.pi / 180) * math.cos(theta * math.pi / 180),
165+
math.sin(theta * math.pi / 180) * math.sin(phi * math.pi / 180),
166+
math.cos(phi * math.pi / 180)
167+
]
168+
169+
sphere_model = Plot(func=fn, allrange=[0, 360], position=(-4, 2, 1), color=(0, 64, 255))
170+
171+
blow_your_head = Plot(
172+
position=(-4, 2, 1), color=(0, 64, 255),
173+
func=lambda x, t: [x, math.cos(x) * math.cos(t), math.cos(t)], allrange=[0, 2*math.pi], interpolate=75
174+
)
175+
176+
```
177+
178+
![Plots](https://i.imgur.com/utZexJ5.png)
179+
180+
181+
## OBJ format
182+
183+
Wawefront format is widely used as a standard in 3D graphics
184+
185+
You can import your model here. Only vertices and faces supported.\
186+
`Model.load_OBJ(cls, path, wireframe=False, **all_model_kwargs)`
187+
188+
You can find examples here [github.com/alecjacobson/common-3d-test-models](https://github.com/alecjacobson/common-3d-test-models)
189+
190+
```python
191+
Model.load_OBJ('beetle.obj.txt', wireframe=True, color=white, position=(-2, 2, -4), scale=3)
192+
```
193+
194+
195+
![Beetle object](https://i.imgur.com/79fy4HK.png)
196+
197+
## Models API
198+
199+
`Models.Model`
200+
201+
| Fields | Description |
202+
| ------------- | ------------- |
203+
| `position` | `tuple=(0, 0, 0)` with x, y, z world coordinates |
204+
| `scale` | `integer(=1)` |
205+
| `color` | `tuple` `(255, 255, 255)` |
206+
| `data` | `list[[x, y, z, [w=1]]]` - Model vertices(points) |
207+
| `faces` | `list[[A, B, C]]` - Defines triangles See: [Mesh and Wireframe](#mesh-and-wireframe) |
208+
| `rasterize` | `bool(=True)` - Rasterize - "fill" an object |
209+
| `shimmering` | `bool(=False)` - color flickering/dancing |
210+
211+
212+
213+
```python
214+
# Initial Model Matrix
215+
model.matrix = Matrix([
216+
[1 * scale, 0, 0, 0],
217+
[0, 1 * scale, 0, 0],
218+
[0, 0, 1 * scale, 0],
219+
[*position, 1]
220+
])
221+
222+
```
223+
224+
## Trajectory API
225+
226+
`Models.Trajectory`
227+
228+
| Fields | Description |
229+
| ------------- | ------------- |
230+
| `func` | `func` Parametrized math function which takes `*args` and returns world respective coordinates `tuple=(x, y, z)` |
231+
232+
To move our object through defined path we can build Trajectory for our object.
233+
You can provide any parametric equation with args.\
234+
World coordinates defined by `func(*args)` tuple output.
235+
236+
#### `model_obj @ translate(x, y, z)`
237+
translates object's model matrix (in world space)
238+
239+
#### `rotate(self, angle_x, angle_y=0, angle_z=0)`
240+
Rotates object relative to particular axis plane. First object translated from the world space back to local origin, then we rotate the object
241+
242+
#### `route(self, trajectory: 'Trajectory', enable_trace=False)`
243+
Set the function-based trajectory routing for the object.
244+
245+
- trajectory `Trajectory` - trajectory state
246+
- enable_trace `bool` - Keep track of i.e. draw trajectory path (breadcrumbs)
247+
248+
#### Example
249+
```python
250+
import math
251+
252+
from play3d.models import Sphere, Trajectory
253+
white = (230, 230, 230)
254+
moving_sphere = Sphere(position=(1, 3, -5), color=white, interpolate=50)
255+
moving_sphere.route(Trajectory.ToAxis.Z(speed=0.02).backwards())
256+
257+
whirling_sphere = Sphere(position=(1, 3, -5), color=white, interpolate=50)
258+
# Already built-in as Trajectory.SineXY(speed=0.1)
259+
whirling_sphere.route(Trajectory(lambda x: [x, math.sin(x)], speed=0.1))
260+
261+
262+
while True: # inside your "render()"
263+
moving_sphere.draw()
264+
whirling_sphere.draw()
265+
```
266+
## Pygame example
267+
268+
```python
269+
import logging
270+
import os
271+
import sys
272+
273+
import pygame
274+
275+
from play3d.models import Model, Grid
276+
from pygame_utils import handle_camera_with_keys # your keyboard control management
277+
from play3d.three_d import Device, Camera
278+
from play3d.utils import capture_fps
279+
280+
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
281+
282+
os.environ["SDL_VIDEO_CENTERED"] = '1'
283+
black, white = (20, 20, 20), (230, 230, 230)
284+
285+
286+
Device.viewport(1024, 768)
287+
pygame.init()
288+
screen = pygame.display.set_mode(Device.get_resolution())
289+
290+
# just for simplicity - array access, we should avoid that
291+
x, y, z = 0, 1, 2
292+
293+
# pygame sdl line is faster than default one
294+
line_adapter = lambda p1, p2, color: pygame.draw.line(screen, color, (p1[x], p1[y]), (p2[x], p2[y]), 1)
295+
put_pixel = lambda x, y, color: pygame.draw.circle(screen, color, (x, y), 1)
296+
297+
Device.set_renderer(put_pixel, line_renderer=line_adapter)
298+
299+
grid = Grid(color=(30, 140, 200), dimensions=(30, 30))
300+
suzanne = Model.load_OBJ('suzanne.obj.txt', position=(3, 2, -7), color=white, rasterize=True)
301+
beetle = Model.load_OBJ('beetle.obj.txt', wireframe=False, color=white, position=(0, 2, -11), scale=3)
302+
beetle.rotate(0, 45, 50)
303+
304+
camera = Camera.get_instance()
305+
# move our camera up and back a bit, from origin
306+
camera.move(y=1, z=2)
307+
308+
309+
@capture_fps
310+
def frame():
311+
if pygame.event.get(pygame.QUIT):
312+
sys.exit(0)
313+
314+
screen.fill(black)
315+
handle_camera_with_keys() # we can move our camera
316+
grid.draw()
317+
beetle.draw()
318+
suzanne.rotate(0, 1, 0).draw()
319+
pygame.display.flip()
320+
321+
322+
while True:
323+
324+
frame()
325+
```

example/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)