Skip to content

Commit 9986ef3

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

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy