├── .gitignore
├── LICENSE
├── README.md
├── TODO.md
├── examples
├── add-delete.py
├── animated_test.py
├── color_change_test.py
├── compare.py
├── first.py
├── hero1.py
├── polygons.py
├── robin_animation.png
└── second.py
├── notes.txt
├── numpy_ecs
├── __init__.py
├── accessors.py
├── components.py
├── global_allocator.py
└── table.py
├── planning.txt
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | #vim
2 | *.swp
3 |
4 | #profiling
5 | ??_profile*
6 | *.prof
7 | *.orig
8 |
9 | # Byte-compiled / optimized / DLL files
10 | __pycache__/
11 | *.py[cod]
12 |
13 | # C extensions
14 | *.so
15 |
16 | # Distribution / packaging
17 | .Python
18 | env/
19 | build/
20 | develop-eggs/
21 | dist/
22 | downloads/
23 | eggs/
24 | .eggs/
25 | lib/
26 | lib64/
27 | parts/
28 | sdist/
29 | var/
30 | *.egg-info/
31 | .installed.cfg
32 | *.egg
33 |
34 | # PyInstaller
35 | # Usually these files are written by a python script from a template
36 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
37 | *.manifest
38 | *.spec
39 |
40 | # Installer logs
41 | pip-log.txt
42 | pip-delete-this-directory.txt
43 |
44 | # Unit test / coverage reports
45 | htmlcov/
46 | .tox/
47 | .coverage
48 | .coverage.*
49 | .cache
50 | nosetests.xml
51 | coverage.xml
52 | *,cover
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 |
61 | # Sphinx documentation
62 | docs/_build/
63 |
64 | # profiling
65 | *.prof
66 |
67 | # PyBuilder
68 | target/
69 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 | {description}
294 | Copyright (C) {year} {fullname}
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | {signature of Ty Coon}, 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
341 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Numpy-ECS
2 | ====================
3 |
4 | Data oriented programming with numpy through an Entity-Component-System model.
5 |
6 | This project was inspired by lack luster performance of rotating a bunch of
7 | polygons in tests towards a game engine. The concept was inspired by:
8 | http://gamesfromwithin.com/data-oriented-design
9 |
10 | Further information on Data Oriented Design:
11 | http://www.dataorienteddesign.com/dodmain/dodmain.html
12 |
13 | Details
14 | =======
15 |
16 | The Allocator is the user's interface with Components, Entities and Systems.
17 |
18 | Components are numpy arrays of the attributes that will make up Entities.
19 | They have a shape and a datatype and once given to the Allocator, they are
20 | abstracted away. ie, an RGB color Component might be:
21 |
22 | colors = Component('color', (3,), np.float32)
23 |
24 | Entities are "instances" defined through composition of Components.
25 | Each instance is actually just an integer (guid) that the Allocator can
26 | use to look up the instance's slice of the Components. Thus Entities are
27 | constructed by allocating values to some subset of Components in the Allocator.
28 | ie, instead of having a class with an `__init__` functions, instances are
29 | created by calling `allocator.add` with some component values defined:
30 |
31 | def add_regular_polygon(n_sides,radius,pos,velocity,
32 | color=(.5,.5,.5),allocator=allocator):
33 | pts = polyOfN(n_sides,radius)
34 | poly_verts = wind_vertices(pts)
35 | pos = position
36 | polygon = {
37 | 'render_verts': [(x+pos[0],y+pos[1],pos[2]) for x,y in poly_verts],
38 | 'color' : [color]*len(poly_verts),
39 | 'position' : pos,
40 | 'velocity' : velocity,
41 | }
42 | guid = allocator.add(polygon)
43 | return guid
44 |
45 | The allocator groups all the entity instances that are composed of the same
46 | Components into "entity classes" so their attributes can be accessed as
47 | continuous slices.
48 |
49 | Systems are functions that operate on the Components entity classes.
50 | These can be implemented through Numpy ufuncs, cython, or
51 | numba.vectorize. Thus, Systems can be fast and CPU multithreaded without
52 | multiprocessing. To apply velocity to every entity that has a velocity:
53 |
54 | def apply_velocity(velocities,positions,dt=1./600):
55 | positions *= velocities*dt
56 |
57 | And a System to render everything that has verts and colors:
58 |
59 | def update_display(render_verts,colors):
60 | gl.glClearColor(0.2, 0.4, 0.5, 1.0)
61 | gl.glBlendFunc (gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
62 | gl.glEnable (gl.GL_BLEND)
63 | gl.glEnable (gl.GL_LINE_SMOOTH);
64 | gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
65 | gl.glEnableClientState(gl.GL_COLOR_ARRAY)
66 |
67 | n = len(render_verts[:])
68 | #TODO verts._buffer.ctypes.data is awkward
69 | gl.glVertexPointer(3, vert_dtype.gl, 0, render_verts[:].ctypes.data)
70 | gl.glColorPointer(3, color_dtype.gl, 0, colors[:].ctypes.data)
71 | gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, n)
72 |
73 | And without any convinience functions, this can be called with the appropriate
74 | sections of the `render_verts` and `color` numpy arrays by doing:
75 |
76 | get_sections = allocator.selectors_from_component_query
77 | draw =('render_verts','color')
78 | sections = get_sections(draw)
79 | update_display(*(sections[name] for name in draw))
80 |
81 | TODO discuss Accessors which examples/hero1.py demonstrates. One can get an
82 | "instance" of an entity that allows access to the underlying arrays for a
83 | single item.
84 |
85 | Performance
86 | ===========
87 |
88 | The difference between `examples/first.py` and `examples/polygons.py` increases
89 | with number of polygons drawn. On my Celeron 2 CPU laptop I get 250 vs 500
90 | FPS with 150 polygons, and 30 vs 150 FPS with 1000 polygons.
91 |
92 | In a more complete game example with pymunk running 2D physics, the original
93 | version ran at 50 FPS and the Numpy-ECS version at 150 FPS. Having pyglet
94 | treat positions and angles as write-only arrays, and rendering treat them as
95 | read-only, the components were created in shared memory and multiprocessing was
96 | used to run the physics and rendering as seperate processes. The shared
97 | memory made up most of the inter process communication (IPC). The result was
98 | 180 FPS for the physics and 600 FPS for the rendering.
99 |
100 | examples: https://vimeo.com/65989831 https://vimeo.com/66736654
101 |
102 | History
103 | =======
104 |
105 | The examples directory shows the evolution of this concept:
106 |
107 | first.py shows my best attempt at using pure python, OOP, and pyglet batches.
108 | Rotation of the polygons consumes 90% of the run time.
109 |
110 | compare.py shows that using numpy on an array has some overhead, but that
111 | the size of the array has much less of an effect on execution time than
112 | with python lists. There is a break even point in the length of data to
113 | be processed. Below this, list comprehensions are faster. Above this,
114 | numpy quickly surpasses pure Python.
115 |
116 | second.py shows that by batching all of the rotation math into one array,
117 | there are substantial performance benefits. But it becomes cumbersome
118 | to interact with instances of the polygons once they are all thrown
119 | together.
120 |
121 | polygons.py implements this same example through the ECS. The overhead
122 | from the higher level interface compared to `second.py` is negligable.
123 |
124 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | allow mask to define fields to exclude. ie: translate if position but NOT angle.
2 | define a __not__ prefix
3 |
4 | improve defragmenting
5 | concatenate contiguous slices
6 | cythonize
7 |
8 | make Systems easier to call
9 |
10 | Create Accessor objects from GUID for object oriented interface when desired.
11 |
12 | Make shared memory IPC and multiprocessing easier and give an example.
13 |
--------------------------------------------------------------------------------
/examples/add-delete.py:
--------------------------------------------------------------------------------
1 | '''
2 | Numpy-ECS example of adding and deleting entitiess
3 |
4 | Hard coded example of the following Entity_class_id table:
5 |
6 | entity_class_id, vertices, color, positions, rotator,
7 |
8 | 1110, 1, 1, 1, 0,
9 | 1111, 1, 1, 1, 1,
10 |
11 | This file is part of Numpy-ECS.
12 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com)
13 |
14 | Numpy-ECS is free software: you can redistribute it and/or modify
15 | it under the terms of the GNU General Public License as published by
16 | the Free Software Foundation, either version 2 of the License, or
17 | (at your option) any later version.
18 |
19 | Data Oriented Python is distributed in the hope that it will be useful,
20 | but WITHOUT ANY WARRANTY; without even the implied warranty of
21 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 | GNU General Public License for more details.
23 |
24 | You should have received a copy of the GNU General Public License
25 | along with this program. If not, see .
26 | '''
27 | import numpy as np
28 | from numpy import sin, cos, pi, sqrt
29 | from math import atan2
30 | import random
31 | import pyglet
32 | from pyglet import gl
33 | from collections import namedtuple
34 | from operator import add
35 |
36 | from numpy_ecs.global_allocator import GlobalAllocator
37 | from numpy_ecs.components import DefraggingArrayComponent as Component
38 |
39 | dtype_tuple = namedtuple('Dtype',('np','gl'))
40 | vert_dtype = dtype_tuple(np.float32,gl.GL_FLOAT)
41 | color_dtype = dtype_tuple(np.float32,gl.GL_FLOAT)
42 |
43 | counter_type = np.dtype([('max_val', np.float32),
44 | ('min_val', np.float32),
45 | ('interval', np.float32),
46 | ('accumulator',np.float32)])
47 |
48 |
49 | allocator = GlobalAllocator((Component('render_verts' , (3,), vert_dtype.np ),
50 | Component('poly_verts' , (5,), color_dtype.np),
51 | Component('color' , (3,), color_dtype.np),
52 | Component('position' , (3,), vert_dtype.np ),
53 | Component('rotator' , (1,), counter_type )),
54 |
55 | allocation_scheme = (
56 | (1,0,1,1,0),
57 | (1,1,1,1,1),
58 | )
59 | )
60 | def polyOfN(n,radius):
61 | '''helper function for making polygons'''
62 | r=radius
63 | if n < 3:
64 | n=3
65 | da = 2*pi/(n) #angle between divisions
66 | return [[r*cos(da*x),r*sin(da*x)] for x in range(int(n))]
67 |
68 | def wind_vertices(pts):
69 | l = len(pts)
70 | #wind up the vertices, so we don't have to do it when speed counts.
71 | #I dont really know which is clock wise and the other counter clock wise, btw
72 | cw = pts[:l//2]
73 | ccw = pts[l//2:][::-1]
74 | flatverts = [None]*(l)
75 | flatverts[::2]=ccw
76 | flatverts[1::2]=cw
77 | wound = [flatverts[0]]+flatverts+[flatverts[-1]]
78 | #prewound vertices can be transformed without care for winding.
79 | #Now, store the vertices in a way that can be translated as efficiently as possible later
80 | #construct list of (x,y,r, x_helper, y_helper)
81 | #note that from alpha to theta, x changes by r*[cos(theta+alpha)-cos(alpha)]
82 | #lets call the initial angle of a vert alpha
83 | #so later, at theta, we want to remember cos(alpha) and sin(alpha)
84 | #they are the helper values
85 | return [(pt[0],pt[1],sqrt(pt[0]**2+pt[1]**2),
86 | cos(atan2(pt[1],pt[0])),sin(atan2(pt[1],pt[0]))) for pt in wound]
87 |
88 | def add_rotating_regular_polygon(n_sides,radius,position,rate,
89 | color=(.5,.5,.5),allocator=allocator):
90 | rot_max = 4*np.pi
91 | rot_min = -rot_max
92 | pts = polyOfN(n_sides,radius)
93 | poly_verts = wind_vertices(pts)
94 | n = len(poly_verts)
95 | polygon = {
96 | 'poly_verts': poly_verts,
97 | 'render_verts': [(0,0,position[2])]*n,
98 | 'color':[color]*n,
99 | 'position':position,
100 | 'rotator':(rot_max,rot_min,rate,0) }
101 | allocator.add(polygon)
102 |
103 | def add_regular_polygon(n_sides,radius,position,
104 | color=(.5,.5,.5),allocator=allocator):
105 | pts = polyOfN(n_sides,radius)
106 | poly_verts = wind_vertices(pts)
107 | n = len(poly_verts)
108 | polygon = {
109 | 'render_verts': [(x+position[0],y+position[1],position[2]) for x,y,_,_,_ in poly_verts],
110 | 'color':[color]*n,
111 | 'position':position,}
112 | allocator.add(polygon)
113 |
114 | def update_rotator(rotator):
115 | arr=rotator
116 | span = arr['max_val'] - arr['min_val']
117 | arr['accumulator'] += arr['interval']
118 | underflow = arr['accumulator'] <= arr['min_val']
119 | arr['accumulator'][underflow] += span[underflow]
120 | overflow = arr['accumulator'] >= arr['max_val']
121 | arr['accumulator'][overflow] -= span[overflow]
122 |
123 |
124 | def update_render_verts(render_verts,poly_verts,positions,rotator,indices=[]):
125 | '''Update vertices to render based on positions and angles communicated
126 | through the data accessors'''
127 |
128 | angles = rotator['accumulator']
129 |
130 | cos_ts, sin_ts = cos(angles), sin(angles)
131 | cos_ts -= 1
132 | #here's a mouthfull. see contruction of initial_data in init. sum-difference folrmula applied
133 | #and simplified. work it out on paper if you don't believe me.
134 | xs, ys, rs, xhelpers, yhelpers = (poly_verts[:,x] for x in range(5))
135 | pts = render_verts
136 |
137 | #print 'shapes:',angles.shape
138 | pts[:,0] = xhelpers*cos_ts[indices]
139 | pts[:,1] = yhelpers*sin_ts[indices]
140 | pts[:,0] -= pts[:,1]
141 | pts[:,0] *= rs
142 | pts[:,0] += xs
143 | pts[:,0] += positions[indices,0]
144 |
145 | pts[:,1] = yhelpers*cos_ts[indices]
146 | tmp = xhelpers*sin_ts[indices]
147 | pts[:,1] += tmp
148 | pts[:,1] *= rs
149 | pts[:,1] += ys
150 | pts[:,1] += positions[indices,1]
151 |
152 | def update_display(render_verts,colors):
153 | gl.glClearColor(0.2, 0.4, 0.5, 1.0)
154 | gl.glBlendFunc (gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
155 | gl.glEnable (gl.GL_BLEND)
156 | gl.glEnable (gl.GL_LINE_SMOOTH);
157 | gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
158 | gl.glEnableClientState(gl.GL_COLOR_ARRAY)
159 |
160 | n = len(render_verts[:])
161 | #TODO verts._buffer.ctypes.data is awkward
162 | gl.glVertexPointer(3, vert_dtype.gl, 0, render_verts[:].ctypes.data)
163 | gl.glColorPointer(3, color_dtype.gl, 0, colors[:].ctypes.data)
164 | gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, n)
165 |
166 |
167 | def add_some(n,n2=None,allocator=allocator):
168 | '''add n random polygons to allocator
169 |
170 | There's two types of polygons. if only n is given, will distribute randomly.
171 | if n2 is given, distibute n to one type and n2 to the other
172 | '''
173 |
174 | assert isinstance(n,int) and (1 if n2 is None else isinstance(n2,int)),\
175 | "n1 and n2 (if given) must be integers"
176 |
177 | if n2 is None:
178 | a = random.random()
179 | n1 = int(n*a)
180 | n2 = n-n1
181 | else:
182 | n1 = n
183 | n2 = 0
184 |
185 | positions = [(x*width,y*height,z) for x,y,z in np.random.random((n1,3))]
186 | rs = [r*50 for r in np.random.random(n1)]
187 | ns = [int(m*10)+3 for m in np.random.random(n1)]
188 | colors = np.random.random((n1,3)).astype(color_dtype.np)
189 | rates = np.random.random(n1)*.01
190 |
191 | for n,r, position, color, rate in zip(ns,rs, positions, colors, rates):
192 | add_rotating_regular_polygon(n,r,position,rate,color)
193 | #from before indicies could be calculated
194 | #indices = np.array(reduce(add, [[x,]*7 for x in range(n1)], []),dtype=np.int)
195 |
196 | positions = [(x*width,y*height,z) for x,y,z in np.random.random((n2,3))]
197 | rs = [r*50 for r in np.random.random(n2)]
198 | ns = [int(m*10)+3 for m in np.random.random(n2)]
199 | colors = np.random.random((n2,3)).astype(color_dtype.np)
200 |
201 | for n_sides,radius, position, color in zip(ns,rs, positions, colors):
202 | add_regular_polygon(n_sides,radius,position,color)
203 |
204 |
205 |
206 | def delete_some(n,allocator=allocator):
207 | guids = random.sample(allocator.guids,n)
208 | for guid in guids:
209 | allocator.delete(guid)
210 |
211 |
212 |
213 | if __name__ == '__main__':
214 |
215 | width, height = 640,480
216 | window = pyglet.window.Window(width, height,vsync=False)
217 | #window = pyglet.window.Window(fullscreen=True,vsync=False)
218 | #width = window.width
219 | #height = window.height
220 | fps_display = pyglet.clock.ClockDisplay()
221 | text = """Numpy ECS"""
222 | label = pyglet.text.HTMLLabel(text, x=10, y=height-10)
223 |
224 | #add some polygons
225 | add_some(100,50)
226 | allocator._defrag()
227 |
228 | get_sections = allocator.selectors_from_component_query
229 |
230 | @window.event
231 | def on_draw():
232 | window.clear()
233 |
234 | allocator._defrag()
235 |
236 | rotator = ('rotator',)
237 | sections = get_sections(rotator)
238 | update_rotator(*(sections[name] for name in rotator))
239 |
240 | render_verts =('render_verts','poly_verts','position','rotator')
241 | broadcast = ('position__to__poly_verts',)
242 | sections = get_sections(render_verts + broadcast)
243 | indices = sections.pop(broadcast[0])
244 | update_render_verts(*(sections[name] for name in render_verts),indices=indices)
245 |
246 | draw =('render_verts','color')
247 | sections = get_sections(draw)
248 | update_display(*(sections[name] for name in draw))
249 |
250 | fps_display.draw()
251 |
252 | pyglet.clock.schedule(lambda _: None)
253 | pyglet.clock.schedule_interval(lambda x,*y: add_some(*y),1,2)
254 | pyglet.clock.schedule_interval(lambda x,*y: delete_some(*y),2,4)
255 | pyglet.app.run()
256 |
257 |
258 |
--------------------------------------------------------------------------------
/examples/animated_test.py:
--------------------------------------------------------------------------------
1 | '''
2 | Numpy-ECS example of animated sprites
3 |
4 | This file is part of Numpy-ECS.
5 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com)
6 |
7 | Numpy-ECS is free software: you can redistribute it and/or modify
8 | it under the terms of the GNU General Public License as published by
9 | the Free Software Foundation, either version 2 of the License, or
10 | (at your option) any later version.
11 |
12 | Data Oriented Python is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | GNU General Public License for more details.
16 |
17 | You should have received a copy of the GNU General Public License
18 | along with this program. If not, see .
19 | '''
20 | import numpy as np
21 | from numpy import sin, cos, pi, sqrt
22 | from math import atan2
23 | import pyglet
24 | from pyglet import gl
25 | from collections import namedtuple
26 | from operator import add
27 |
28 | from numpy_ecs.global_allocator import GlobalAllocator
29 | from numpy_ecs.components import DefraggingArrayComponent as Component
30 |
31 | #for reproduceable output
32 | seed = 123456789
33 | np.random.seed(seed)
34 |
35 | window_width, window_height = 640,480
36 |
37 | dtype_tuple = namedtuple('Dtype',('np','gl'))
38 | vert_dtype = dtype_tuple(np.float32,gl.GL_FLOAT)
39 | tex_dtype = dtype_tuple(np.float32,gl.GL_FLOAT)
40 |
41 | animator_type = np.dtype([('min_val', np.int32),
42 | ('max_val', np.int32),
43 | ('interval', np.int32),
44 | ('accumulator',np.int32)])
45 |
46 |
47 | allocator = GlobalAllocator((Component('render_verts' , (3,), vert_dtype.np ),
48 | Component('tex_coords' , (3,), tex_dtype.np ),
49 | Component('poly_verts' , (5,), vert_dtype.np ),
50 | Component('position' , (3,), vert_dtype.np ),
51 | Component('velocity' , (1,), vert_dtype.np ),
52 | Component('animator' , (1,), animator_type )),
53 |
54 | allocation_scheme = (
55 | (1,1,1,1,1,1),
56 | )
57 | )
58 | def wind_pts(pts,reshape=False):
59 | '''take clockwise or counter clockwise points and wind them as would
60 | be needed for rendering as triangle strips'''
61 | if reshape:
62 | pts = zip(pts[::3],pts[1::3],pts[2::3])
63 | l = len(pts)
64 | cw = pts[:l//2]
65 | ccw = pts[l//2:][::-1]
66 | flatverts = [None]*(l)
67 | flatverts[::2]=ccw
68 | flatverts[1::2]=cw
69 | return [flatverts[0]]+flatverts+[flatverts[-1]]
70 |
71 | image_name='robin_animation.png'
72 | rows, cols = 5,5
73 | raw = pyglet.image.load(image_name)
74 | raw_seq = pyglet.image.ImageGrid(raw, rows, cols)
75 | items = raw_seq.get_texture_sequence().items[:]
76 | #select animation in order
77 | items = items[20:]+items[15:20]+items[10:15]+items[5:10]+items[0:1]
78 | bird_texture = items[0].texture
79 | animation_lookup = np.array([wind_pts(x.tex_coords,reshape=True) for x in items])
80 |
81 | def wind_vertices(pts):
82 | '''wind pts and pre-compute data that is helpful for transformations'''
83 | wound = wind_pts(pts)
84 | #prewound vertices can be transformed without care for winding.
85 | #Now, store the vertices in a way that can be translated as efficiently as possible later
86 | #construct list of (x,y,r, x_helper, y_helper)
87 | #note that from alpha to theta, x changes by r*[cos(theta+alpha)-cos(alpha)]
88 | #lets call the initial angle of a vert alpha
89 | #so later, at theta, we want to remember cos(alpha) and sin(alpha)
90 | #they are the helper values
91 | return [(pt[0],pt[1],sqrt(pt[0]**2+pt[1]**2),
92 | cos(atan2(pt[1],pt[0])),sin(atan2(pt[1],pt[0]))) for pt in wound]
93 |
94 | def add_sprite(width,height,position,rate,anim_start=0, first_frame=0,
95 | anim_stop=1, anim_step=1, allocator=allocator):
96 | width /= 2.
97 | height /= 2.
98 | pts = ( (-width, -height), ( width, -height),
99 | ( width, height), (-width, height), )
100 |
101 | poly_verts = wind_vertices(pts)
102 |
103 | n = len(poly_verts)
104 | polygon = {
105 | 'poly_verts': poly_verts,
106 | 'tex_coords': [(0,0,0)]*n, #allocate empty space. system will fill
107 | 'render_verts': [(0,0,position[2])]*n,
108 | 'velocity': rate,
109 | 'position': position,
110 | 'animator': (anim_start,anim_stop,anim_step,first_frame) }
111 | allocator.add(polygon)
112 |
113 |
114 | def update_position(velocity,position):
115 | global window_width
116 | position[:,0] -= velocity
117 | #wrap around
118 | position[:,0][position[:,0] < -20] = window_width + 20
119 |
120 |
121 | def update_animation(tex_coords,animator,animation_lookup=animation_lookup):
122 | arr = animator
123 | span = arr['max_val'] - arr['min_val']
124 | arr['accumulator'] += arr['interval']
125 | underflow = arr['accumulator'] <= arr['min_val']
126 | arr['accumulator'][underflow] += span[underflow]
127 | overflow = arr['accumulator'] >= arr['max_val']
128 | arr['accumulator'][overflow] -= span[overflow]
129 |
130 | temp = animation_lookup[arr['accumulator']]
131 | temp.shape = (temp.shape[0]*temp.shape[1],temp.shape[2])
132 | tex_coords[:] = temp
133 |
134 |
135 | def update_render_verts(render_verts,poly_verts,positions,indices=[]):
136 | '''Update vertices to render based on positions'''
137 |
138 | render_verts[:,:2] = poly_verts[:,:2]
139 | render_verts[:,:2] += positions[:,:2][indices]
140 | #render_verts[:,2] = positions[indices][:,2]
141 |
142 | def update_display(verts,tex_coords,texture=bird_texture):
143 | gl.glClearColor(0.2, 0.4, 0.5, 1.0)
144 |
145 | gl.glEnable(texture.target)
146 | gl.glBindTexture(texture.target, texture.id)
147 |
148 | gl.glPushAttrib(gl.GL_COLOR_BUFFER_BIT)
149 |
150 | gl.glEnable(gl.GL_ALPHA_TEST)
151 | gl.glAlphaFunc (gl.GL_GREATER, .1)
152 | #gl.glEnable(gl.GL_BLEND)
153 | #gl.glBlendFunc (gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
154 | gl.glEnable(gl.GL_DEPTH_TEST)
155 |
156 | gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
157 | gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY)
158 |
159 | n=len(verts[:])
160 | #TODO verts._buffer.ctypes.data is awkward
161 | gl.glVertexPointer(3, vert_dtype.gl, 0, verts[:].ctypes.data)
162 | gl.glTexCoordPointer(3, tex_dtype.gl, 0, tex_coords[:].ctypes.data)
163 | gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, n)
164 | #unset state
165 | gl.glPopAttrib()
166 | gl.glDisable(texture.target)
167 |
168 |
169 | if __name__ == '__main__':
170 |
171 | window = pyglet.window.Window(window_width, window_height,vsync=False)
172 | #window = pyglet.window.Window(fullscreen=True,vsync=False)
173 | #width = window.width
174 | #height = window.height
175 | fps_display = pyglet.clock.ClockDisplay()
176 | text = """Numpy ECS"""
177 | label = pyglet.text.HTMLLabel(text, x=10, y = window_height-10)
178 |
179 | n=50
180 |
181 | positions = np.random.random((n,3))
182 | positions[:,0] *= window_width
183 | positions[:,1] *= window_height
184 | sizes = (positions[:,2])*200
185 | anim_starts = np.random.random_integers(0,20,size=n)
186 |
187 | for size, position, start in zip(sizes, positions, anim_starts):
188 | add_sprite(size,size,position,size*.001,first_frame=start,anim_stop=20)
189 |
190 | allocator._defrag()
191 |
192 | get_sections = allocator.selectors_from_component_query
193 |
194 | def apply_animation(this):
195 | animation =('tex_coords','animator')
196 | sections = get_sections(animation)
197 | update_animation(*(sections[name] for name in animation))
198 |
199 | @window.event
200 | def on_draw():
201 | window.clear()
202 |
203 | mover = ('velocity','position',)
204 | sections = get_sections(mover)
205 | update_position(*(sections[name] for name in mover))
206 |
207 | render_verts =('render_verts','poly_verts','position')
208 | broadcast = ('position__to__poly_verts',)
209 | sections = get_sections(render_verts + broadcast)
210 | indices = sections.pop(broadcast[0])
211 | update_render_verts(*(sections[name] for name in render_verts),indices=indices)
212 |
213 | draw =('render_verts','tex_coords')
214 | sections = get_sections(draw)
215 | update_display(*(sections[name] for name in draw))
216 |
217 | fps_display.draw()
218 |
219 | pyglet.clock.schedule(lambda _: None)
220 | pyglet.clock.schedule_interval(apply_animation,.05)
221 |
222 | pyglet.app.run()
223 |
224 |
225 |
--------------------------------------------------------------------------------
/examples/color_change_test.py:
--------------------------------------------------------------------------------
1 | '''
2 | Numpy-ECS example implementing rotation and color changing
3 |
4 | Hard coded example of the following Entity_class_id table:
5 |
6 | entity_class_id, vertices, color, positions, rotator, color_changer
7 |
8 | 11100, 1, 1, 1, 0, 0
9 | 11110, 1, 1, 1, 1, 0
10 | 11111, 1, 1, 1, 1, 1
11 | 11101, 1, 1, 1, 0, 1
12 |
13 | This file is part of Numpy-ECS.
14 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com)
15 |
16 | Data Oreinted Python is free software: you can redistribute it and/or modify
17 | it under the terms of the GNU General Public License as published by
18 | the Free Software Foundation, either version 2 of the License, or
19 | (at your option) any later version.
20 |
21 | Data Oriented Python is distributed in the hope that it will be useful,
22 | but WITHOUT ANY WARRANTY; without even the implied warranty of
23 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 | GNU General Public License for more details.
25 |
26 | You should have received a copy of the GNU General Public License
27 | along with this program. If not, see .
28 |
29 | '''
30 |
31 | import numpy as np
32 | from numpy import sin, cos, pi, sqrt
33 | from math import atan2
34 | import pyglet
35 | from pyglet import gl
36 | from collections import namedtuple
37 | from operator import add
38 |
39 | from numpy_ecs.global_allocator import GlobalAllocator
40 | from numpy_ecs.components import DefraggingArrayComponent as Component
41 |
42 | dtype_tuple = namedtuple('Dtype',('np','gl'))
43 | vert_dtype = dtype_tuple(np.float32,gl.GL_FLOAT)
44 | color_dtype = dtype_tuple(np.float32,gl.GL_FLOAT)
45 |
46 | counter_type = np.dtype([('max_val', np.float32),
47 | ('min_val', np.float32),
48 | ('interval', np.float32),
49 | ('accumulator',np.float32)])
50 |
51 | color_type = np.dtype([(c,color_dtype.np) for c in "RGB"])
52 |
53 | color_changer_type = np.dtype([('max_val', np.float32),
54 | ('min_val', np.float32),
55 | ('interval', np.float32),
56 | ('accumulator',np.float32),
57 | ('color', color_type)])
58 |
59 |
60 | allocator = GlobalAllocator((Component('render_verts' , (3,), vert_dtype.np ),
61 | Component('poly_verts' , (5,), color_dtype.np),
62 | Component('color' , (3,), color_dtype.np),
63 | Component('position' , (3,), vert_dtype.np ),
64 | Component('rotator' , (1,), counter_type ),
65 | Component('color_changer', (1,), color_changer_type)),
66 |
67 | allocation_scheme = (
68 | (1,0,1,1,0,0),
69 | (1,1,1,1,1,0),
70 | (1,1,1,1,1,1),
71 | (1,0,1,1,0,1),
72 | )
73 | )
74 | def polyOfN(n,radius):
75 | '''helper function for making polygons'''
76 | r=radius
77 | if n < 3:
78 | n=3
79 | da = 2*pi/(n) #angle between divisions
80 | return [[r*cos(da*x),r*sin(da*x)] for x in range(int(n))]
81 |
82 | def wind_vertices(pts):
83 | l = len(pts)
84 | #wind up the vertices, so we don't have to do it when speed counts.
85 | #I dont really know which is clock wise and the other counter clock wise, btw
86 | cw = pts[:l//2]
87 | ccw = pts[l//2:][::-1]
88 | flatverts = [None]*(l)
89 | flatverts[::2]=ccw
90 | flatverts[1::2]=cw
91 | wound = [flatverts[0]]+flatverts+[flatverts[-1]]
92 | #prewound vertices can be transformed without care for winding.
93 | #Now, store the vertices in a way that can be translated as efficiently as possible later
94 | #construct list of (x,y,r, x_helper, y_helper)
95 | #note that from alpha to theta, x changes by r*[cos(theta+alpha)-cos(alpha)]
96 | #lets call the initial angle of a vert alpha
97 | #so later, at theta, we want to remember cos(alpha) and sin(alpha)
98 | #they are the helper values
99 | return [(pt[0],pt[1],sqrt(pt[0]**2+pt[1]**2),
100 | cos(atan2(pt[1],pt[0])),sin(atan2(pt[1],pt[0]))) for pt in wound]
101 |
102 | def add_rotating_regular_polygon(n_sides,radius,position,rate,
103 | color=(.5,.5,.5),allocator=allocator):
104 | rot_max = 4*np.pi
105 | rot_min = -rot_max
106 | pts = polyOfN(n_sides,radius)
107 | poly_verts = wind_vertices(pts)
108 | n = len(poly_verts)
109 | polygon = {
110 | 'poly_verts': poly_verts,
111 | 'render_verts': [(0,0,position[2])]*n,
112 | 'color':[color]*n,
113 | 'position':position,
114 | 'rotator':(rot_max,rot_min,rate,0) }
115 | allocator.add(polygon)
116 |
117 | def add_rotating_color_changing_polygon(n_sides,radius,position,rate,
118 | color=(.5,.5,.5),allocator=allocator):
119 | rot_max = 4*np.pi
120 | rot_min = -rot_max
121 | pts = polyOfN(n_sides,radius)
122 | poly_verts = wind_vertices(pts)
123 | n = len(poly_verts)
124 | polygon = {
125 | 'poly_verts': poly_verts,
126 | 'render_verts': [(0,0,position[2])]*n,
127 | 'color':[color]*n,
128 | 'position':position,
129 | 'rotator':(rot_max,rot_min,rate,0),
130 | 'color_changer':(1,0,rate/10,1,color)}
131 |
132 | allocator.add(polygon)
133 |
134 | def add_regular_polygon(n_sides,radius,position,
135 | color=(.5,.5,.5),allocator=allocator):
136 | pts = polyOfN(n_sides,radius)
137 | poly_verts = wind_vertices(pts)
138 | n = len(poly_verts)
139 | polygon = {
140 | 'render_verts': [(x+position[0],y+position[1],position[2]) for x,y,_,_,_ in poly_verts],
141 | 'color':[color]*n,
142 | 'position':position,}
143 | allocator.add(polygon)
144 |
145 | def update_rotator(rotator):
146 | arr=rotator
147 | span = arr['max_val'] - arr['min_val']
148 | arr['accumulator'] += arr['interval']
149 | underflow = arr['accumulator'] <= arr['min_val']
150 | arr['accumulator'][underflow] += span[underflow]
151 | overflow = arr['accumulator'] >= arr['max_val']
152 | arr['accumulator'][overflow] -= span[overflow]
153 |
154 | def update_colors(color,color_changer,indices=[]):
155 | accumulator = color_changer['accumulator']
156 | interval = color_changer['interval']
157 | #ready = color_changer['ready_flag']
158 | poly_color = color_changer['color']
159 | max_value = color_changer['max_val']
160 | min_value = color_changer['min_val']
161 |
162 |
163 | accumulator += interval
164 |
165 | underflow = accumulator < min_value #must not be <=
166 | #ready[underflow] = True
167 | accumulator[underflow] = max_value[underflow]
168 |
169 | overflow = accumulator > max_value #must be >, not >=
170 | #ready[overflow] = True
171 | accumulator[overflow] = min_value[overflow]
172 |
173 | #TODO using record arrays is not compatible with pure multi dim arrays
174 | # whats a better idiom?
175 | R = poly_color['R'] * accumulator
176 | G = poly_color['G'] * accumulator
177 | B = poly_color['B'] * accumulator
178 | color[:,0] = R[indices]
179 | color[:,1] = G[indices]
180 | color[:,2] = B[indices]
181 |
182 | def update_render_verts(render_verts,poly_verts,positions,rotator,indices=[]):
183 | '''Update vertices to render based on positions and angles communicated
184 | through the data accessors'''
185 |
186 | angles = rotator['accumulator']
187 |
188 | cos_ts, sin_ts = cos(angles), sin(angles)
189 | cos_ts -= 1
190 | #here's a mouthfull. see contruction of initial_data in init. sum-difference folrmula applied
191 | #and simplified. work it out on paper if you don't believe me.
192 | xs, ys, rs, xhelpers, yhelpers = (poly_verts[:,x] for x in range(5))
193 | pts = render_verts
194 |
195 | #print 'shapes:',angles.shape
196 | pts[:,0] = xhelpers*cos_ts[indices]
197 | pts[:,1] = yhelpers*sin_ts[indices]
198 | pts[:,0] -= pts[:,1]
199 | pts[:,0] *= rs
200 | pts[:,0] += xs
201 | pts[:,0] += positions[indices,0]
202 |
203 | pts[:,1] = yhelpers*cos_ts[indices]
204 | tmp = xhelpers*sin_ts[indices]
205 | pts[:,1] += tmp
206 | pts[:,1] *= rs
207 | pts[:,1] += ys
208 | pts[:,1] += positions[indices,1]
209 |
210 | def update_display(render_verts,colors):
211 | gl.glClearColor(0.2, 0.4, 0.5, 1.0)
212 | gl.glBlendFunc (gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
213 | gl.glEnable (gl.GL_BLEND)
214 | gl.glEnable (gl.GL_LINE_SMOOTH);
215 | gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
216 | gl.glEnableClientState(gl.GL_COLOR_ARRAY)
217 |
218 | n = len(render_verts[:])
219 | #TODO verts._buffer.ctypes.data is awkward
220 | gl.glVertexPointer(3, vert_dtype.gl, 0, render_verts[:].ctypes.data)
221 | gl.glColorPointer(3, color_dtype.gl, 0, colors[:].ctypes.data)
222 | gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, n)
223 |
224 | if __name__ == '__main__':
225 |
226 |
227 | ########################
228 | #
229 | # Instatiate pyglet window
230 | #
231 | #######################
232 |
233 | width, height = 640,480
234 | window = pyglet.window.Window(width, height,vsync=False)
235 | #window = pyglet.window.Window(fullscreen=True,vsync=False)
236 | #width = window.width
237 | #height = window.height
238 | fps_display = pyglet.clock.ClockDisplay()
239 | text = """Numpy ECS"""
240 | label = pyglet.text.HTMLLabel(text, x=10, y=height-10)
241 |
242 | ########################
243 | #
244 | # create polygons
245 | #
246 | #######################
247 | n1=50
248 |
249 | positions = [(x*width,y*height,z) for x,y,z in np.random.random((n1,3))]
250 | rs = [r*50 for r in np.random.random(n1)]
251 | ns = [int(m*10)+3 for m in np.random.random(n1)]
252 | colors = np.random.random((n1,3)).astype(color_dtype.np)
253 | rates = np.random.random(n1)*.01
254 |
255 | for n,r, position, color, rate in zip(ns,rs, positions, colors, rates):
256 | add_rotating_regular_polygon(n,r,position,rate,color)
257 |
258 | n1=50
259 |
260 | positions = [(x*width,y*height,z) for x,y,z in np.random.random((n1,3))]
261 | rs = [r*50 for r in np.random.random(n1)]
262 | ns = [int(m*10)+3 for m in np.random.random(n1)]
263 | colors = np.random.random((n1,3)).astype(color_dtype.np)
264 | rates = np.random.random(n1)*.01
265 |
266 | for n,r, position, color, rate in zip(ns,rs, positions, colors, rates):
267 | add_rotating_color_changing_polygon(n,r,position,rate,color)
268 |
269 | n2=50
270 |
271 | positions = [(x*width,y*height,z) for x,y,z in np.random.random((n2,3))]
272 | rs = [r*50 for r in np.random.random(n2)]
273 | ns = [int(m*10)+3 for m in np.random.random(n2)]
274 | colors = np.random.random((n2,3)).astype(color_dtype.np)
275 |
276 | for n_sides,radius, position, color in zip(ns,rs, positions, colors):
277 | add_regular_polygon(n_sides,radius,position,color)
278 |
279 | allocator._defrag()
280 |
281 | ########################
282 | #
283 | # Run game loop forever
284 | #
285 | #######################
286 | get_sections = allocator.selectors_from_component_query
287 |
288 | @window.event
289 | def on_draw():
290 | window.clear()
291 |
292 | rotator = ('rotator',)
293 | sections = get_sections(rotator)
294 | update_rotator(*(sections[name] for name in rotator))
295 |
296 | color_changer =('color','color_changer')
297 | broadcast = ('color_changer__to__color',)
298 | sections = get_sections(color_changer + broadcast)
299 | indices = sections.pop(broadcast[0])
300 | update_colors(*(sections[name] for name in color_changer),indices=indices)
301 |
302 | render_verts =('render_verts','poly_verts','position','rotator')
303 | broadcast = ('position__to__poly_verts',)
304 | sections = get_sections(render_verts + broadcast)
305 | indices = sections.pop(broadcast[0])
306 | update_render_verts(*(sections[name] for name in render_verts),indices=indices)
307 |
308 | draw =('render_verts','color')
309 | sections = get_sections(draw)
310 | update_display(*(sections[name] for name in draw))
311 |
312 | fps_display.draw()
313 |
314 | pyglet.clock.schedule(lambda _: None)
315 |
316 | pyglet.app.run()
317 |
318 |
319 |
--------------------------------------------------------------------------------
/examples/compare.py:
--------------------------------------------------------------------------------
1 | '''
2 | quick test to compare numpy math vs list comprehensions as a function
3 | of size of data.
4 |
5 | This file is part of Data Oriented Python.
6 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com)
7 |
8 | Data Oreinted Python is free software: you can redistribute it and/or modify
9 | it under the terms of the GNU General Public License as published by
10 | the Free Software Foundation, either version 2 of the License, or
11 | (at your option) any later version.
12 |
13 | Data Oriented Python is distributed in the hope that it will be useful,
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | GNU General Public License for more details.
17 |
18 | You should have received a copy of the GNU General Public License
19 | along with this program. If not, see .
20 |
21 | '''
22 | import numpy as np
23 | from math import pi, sin, cos,atan2,sqrt
24 | import time
25 |
26 | def gen_initiald(n):
27 |
28 | pts = np.random.random((n,2)).astype(np.float32)
29 | list_data = [(pt[0],pt[1],sqrt(pt[0]**2+pt[1]**2),cos(atan2(pt[1],pt[0])),sin(atan2(pt[1],pt[0]))) for pt in pts]
30 | array_data = np.array(list_data)
31 | return list_data, array_data
32 |
33 | def list_rotate(initiald):
34 | px, py = (10,10)
35 | cost, sint = cos(.5), sin(.5)
36 | #here's a mouthfull. see contruction of initial_data in init. sum-difference folrmula applied
37 | #and simplified. work it out on paper if you don't believe me.
38 | pts = [(px+x+r*(xhelper*(cost-1)-sint*yhelper),py+y+r*(yhelper*(cost-1)+sint*xhelper)) for x,y,r,xhelper,yhelper in initiald]
39 | #flatten and return as a tuple for vertbuffer
40 | return tuple(map(int,[val for subl in pts for val in subl]))
41 |
42 | def array_rotate(initiald):
43 | px, py = (0,0)
44 | cost, sint = cos(.5), sin(.5)
45 | xs, ys, rs, xhelpers, yhelpers = (initiald[:,x] for x in range(5))
46 |
47 | pts = np.empty((len(xs),2),dtype=np.float32)
48 |
49 | pts[:,0] = xhelpers*(cost-1)
50 | pts[:,1] = yhelpers*sint
51 | pts[:,0] -= pts[:,1]
52 | pts[:,0] *= rs
53 | pts[:,0] += xs
54 | pts[:,0] += px
55 |
56 | pts[:,1] = yhelpers*(cost-1)
57 | tmp = xhelpers*sint
58 | pts[:,1] += tmp
59 | pts[:,1] *= rs
60 | pts[:,1] += ys
61 | pts[:,1] += py
62 |
63 | #flatten and return as a tuple for vertbuffer
64 | #return tuple(map(int,[val for subl in pts for val in subl]))
65 | pts.shape = ( reduce(lambda xx,yy: xx*yy, pts.shape), )
66 | return pts.astype(np.int32)
67 |
68 | for n in [5,10,25,50,100,500,1000]:
69 | lst,arr = gen_initiald(n)
70 |
71 | start = time.time()
72 | trash = list_rotate(lst)
73 | end = time.time()
74 | print "did list of %s in %s" % (n, end-start)
75 |
76 | start = time.time()
77 | trash = array_rotate(arr)
78 | end = time.time()
79 | print "did array of %s in %s" % (n, end-start)
80 |
81 | print "\n"
82 |
--------------------------------------------------------------------------------
/examples/first.py:
--------------------------------------------------------------------------------
1 | '''
2 | Example using batches instead of Numpy-ECS
3 |
4 | Heavy math happens on each object one at a time.
5 |
6 | (object oriented rather than data oriented).
7 |
8 | This file is part of Numpy-ECS.
9 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com)
10 |
11 | Numpy-ECS is free software: you can redistribute it and/or modify
12 | it under the terms of the GNU General Public License as published by
13 | the Free Software Foundation, either version 2 of the License, or
14 | (at your option) any later version.
15 |
16 | Data Oriented Python is distributed in the hope that it will be useful,
17 | but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | GNU General Public License for more details.
20 |
21 | You should have received a copy of the GNU General Public License
22 | along with this program. If not, see .
23 |
24 | '''
25 |
26 |
27 | from __future__ import absolute_import, division, print_function, unicode_literals
28 | import numpy as np
29 | import pyglet
30 | from pyglet import gl
31 | from pyglet.graphics import Batch
32 | from math import pi, sin, cos,atan2,sqrt
33 | import time
34 |
35 | #Limit run time for profiling
36 | run_for = 15 #seconds to run test for
37 | def done_yet(duration = run_for, start=time.time()):
38 | return time.time()-start > duration
39 |
40 | #Set up window
41 | width, height = 640,480
42 | window = pyglet.window.Window(width, height,vsync=False)
43 | #window = pyglet.window.Window(fullscreen=True,vsync=False)
44 | #width = window.width
45 | #height = window.height
46 | fps_display = pyglet.clock.ClockDisplay()
47 | text = """Unoptimized"""
48 | label = pyglet.text.HTMLLabel(text, x=10, y=height-10)
49 |
50 | main_batch = Batch()
51 |
52 | def draw():
53 | global main_batch
54 | gl.glClearColor(0.2, 0.4, 0.5, 1.0)
55 | gl.glBlendFunc (gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
56 | gl.glEnable (gl.GL_BLEND)
57 | gl.glEnable (gl.GL_LINE_SMOOTH);
58 | gl.glLineWidth (3)
59 |
60 | main_batch.draw()
61 |
62 | #helper hunction for making polygons
63 | def polyOfN(radius,n):
64 | r=radius
65 | if n < 3:
66 | n=3
67 | da = 2*pi/(n) #angle between divisions
68 | return [[r*cos(da*x),r*sin(da*x)] for x in range(n)]
69 |
70 |
71 | class Convex(object):
72 | '''Convex polygons for rendering'''
73 | global main_batch
74 |
75 | def __init__(self,pts, position=None, color=None, radius=0):
76 | if position is None:
77 | position = (width//2, height//2)
78 | self.position = position
79 | if color is None:
80 | color = [60,0,0]
81 | self.color = color
82 | self.pts=pts
83 | self.initializeVertices()
84 |
85 | def initializeVertices(self):
86 | pts = self.pts
87 | l = len(pts)
88 | #wind up the vertices, so we don't have to do it when speed counts.
89 | #I dont really know which is clock wise and the other counter clock wise, btw
90 | cw = pts[:l//2]
91 | ccw = pts[l//2:][::-1]
92 | flatverts = [None]*(l)
93 | flatverts[::2]=ccw
94 | flatverts[1::2]=cw
95 | wound = [flatverts[0]]+flatverts+[flatverts[-1]]
96 | #prewound vertices can be transformed without care for winding.
97 | #Now, store the vertices in a way that can be translated as efficiently as possible later
98 | #construct list of (x,y,r, x_helper, y_helper)
99 | #note that from alpha to theta, x changes by r*[cos(theta+alpha)-cos(alpha)]
100 | #lets call the initial angle of a vert alpha
101 | #so later, at theta, we want to remember cos(alpha) and sin(alpha)
102 | #they are the helper values
103 | self.initial_data = [(pt[0],pt[1],sqrt(pt[0]**2+pt[1]**2),cos(atan2(pt[1],pt[0])),sin(atan2(pt[1],pt[0]))) for pt in wound]
104 | verts = self.rotate(0)
105 | self.n=len(verts)//2
106 | self.set_colors()
107 | self.vertlist = main_batch.add(self.n, gl.GL_TRIANGLE_STRIP,None,
108 | ('v2i,',verts),('c3b',self.colors))
109 |
110 | def set_colors(self):
111 | self.colors = self.color*self.n
112 |
113 | def rotate(self,theta):
114 | px, py = self.position
115 | initiald = self.initial_data
116 | cost, sint = cos(theta), sin(theta)
117 | #here's a mouthfull. see contruction of initial_data in init. sum-difference folrmula applied
118 | #and simplified. work it out on paper if you don't believe me.
119 | pts = [(px+x+r*(xhelper*(cost-1)-sint*yhelper),py+y+r*(yhelper*(cost-1)+sint*xhelper)) for x,y,r,xhelper,yhelper in initiald]
120 | #flatten and return as a tuple for vertbuffer
121 | return tuple(map(int,[val for subl in pts for val in subl]))
122 |
123 | def rotate_and_render(self,angle):
124 | self.vertlist.vertices = self.rotate(angle)
125 |
126 | def update_colors(self):
127 | ##print(self.vertlist.colors)
128 | self.vertlist.colors=self.colors
129 |
130 |
131 | n=1000
132 | positions = [(x*width,y*height) for x,y in np.random.random((n,2))]
133 | poly_args = [(r*50,int(m*10)+3) for r,m in np.random.random((n,2))]
134 | colors = [map(lambda x: int(x*255),vals) for vals in np.random.random((n,3))]
135 |
136 | ents = [Convex(polyOfN(*pargs),position=pos, color=col) for pargs,pos,col in zip(poly_args,positions,colors)]
137 |
138 | angles= [0]*n
139 | rates = list(np.random.random(n)*.02)
140 | @window.event
141 | def on_draw():
142 | global angle
143 |
144 | if done_yet():
145 | pyglet.app.exit()
146 |
147 | window.clear()
148 | for i, ent in enumerate(ents):
149 | angles[i]+=rates[i]
150 | ent.rotate_and_render(angles[i])
151 | draw()
152 | fps_display.draw()
153 |
154 |
155 | #pyglet.clock.set_fps_limit(60)
156 | pyglet.clock.schedule(lambda _: None)
157 |
158 | pyglet.app.run()
159 |
160 |
161 |
--------------------------------------------------------------------------------
/examples/hero1.py:
--------------------------------------------------------------------------------
1 | '''
2 | Numpy-ECS example of using an Accessor to manipulate an entity instance
3 | in an object oriented manner
4 |
5 | Hard coded example of the following Entity_class_id table:
6 |
7 | entity_class_id, vertices, color, positions, rotator,
8 |
9 | 1110, 1, 1, 1, 0,
10 | 1111, 1, 1, 1, 1,
11 |
12 | This file is part of Numpy-ECS.
13 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com)
14 |
15 | Numpy-ECS is free software: you can redistribute it and/or modify
16 | it under the terms of the GNU General Public License as published by
17 | the Free Software Foundation, either version 2 of the License, or
18 | (at your option) any later version.
19 |
20 | Data Oriented Python is distributed in the hope that it will be useful,
21 | but WITHOUT ANY WARRANTY; without even the implied warranty of
22 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 | GNU General Public License for more details.
24 |
25 | You should have received a copy of the GNU General Public License
26 | along with this program. If not, see .
27 | '''
28 | from __future__ import absolute_import, division, print_function
29 | #python version compatability
30 | import sys
31 | if sys.version_info < (3,0):
32 | from future_builtins import zip, map
33 | import numpy as np
34 | from numpy import sin, cos, pi, sqrt
35 | from math import atan2
36 | import pyglet
37 | from pyglet.window.key import LEFT, RIGHT, UP, DOWN
38 | from pyglet import gl
39 | from collections import namedtuple
40 | from operator import add
41 | from numpy_ecs.global_allocator import GlobalAllocator
42 | from numpy_ecs.components import DefraggingArrayComponent as Component
43 |
44 | #for reproduceable output
45 | seed = 123456789
46 | np.random.seed(seed)
47 |
48 | dtype_tuple = namedtuple('Dtype',('np','gl'))
49 | vert_dtype = dtype_tuple(np.float32,gl.GL_FLOAT)
50 | color_dtype = dtype_tuple(np.float32,gl.GL_FLOAT)
51 |
52 | counter_type = np.dtype([('max_val', np.float32),
53 | ('min_val', np.float32),
54 | ('interval', np.float32),
55 | ('accumulator',np.float32)])
56 |
57 |
58 | allocator = GlobalAllocator((Component('render_verts' , (3,), vert_dtype.np ),
59 | Component('poly_verts' , (5,), color_dtype.np),
60 | Component('color' , (3,), color_dtype.np),
61 | Component('position' , (3,), vert_dtype.np ),
62 | Component('rotator' , (1,), counter_type )),
63 |
64 | allocation_scheme = (
65 | (1,0,1,1,0),
66 | (1,1,1,1,1),
67 | )
68 | )
69 | def polyOfN(n,radius):
70 | '''helper function for making polygons'''
71 | r=radius
72 | if n < 3:
73 | n=3
74 | da = 2*pi/(n) #angle between divisions
75 | return [[r*cos(da*x),r*sin(da*x)] for x in range(int(n))]
76 |
77 | def wind_vertices(pts):
78 | l = len(pts)
79 | #wind up the vertices, so we don't have to do it when speed counts.
80 | #I dont really know which is clock wise and the other counter clock wise, btw
81 | cw = pts[:l//2]
82 | ccw = pts[l//2:][::-1]
83 | flatverts = [None]*(l)
84 | flatverts[::2]=ccw
85 | flatverts[1::2]=cw
86 | wound = [flatverts[0]]+flatverts+[flatverts[-1]]
87 | #prewound vertices can be transformed without care for winding.
88 | #Now, store the vertices in a way that can be translated as efficiently as possible later
89 | #construct list of (x,y,r, x_helper, y_helper)
90 | #note that from alpha to theta, x changes by r*[cos(theta+alpha)-cos(alpha)]
91 | #lets call the initial angle of a vert alpha
92 | #so later, at theta, we want to remember cos(alpha) and sin(alpha)
93 | #they are the helper values
94 | return [(pt[0],pt[1],sqrt(pt[0]**2+pt[1]**2),
95 | cos(atan2(pt[1],pt[0])),sin(atan2(pt[1],pt[0]))) for pt in wound]
96 |
97 | def add_rotating_regular_polygon(n_sides,radius,position,rate,
98 | color=(.5,.5,.5),allocator=allocator):
99 | rot_max = 4*np.pi
100 | rot_min = -rot_max
101 | pts = polyOfN(n_sides,radius)
102 | poly_verts = wind_vertices(pts)
103 | n = len(poly_verts)
104 | polygon = {
105 | 'poly_verts': poly_verts,
106 | 'render_verts': [(0,0,position[2])]*n,
107 | 'color':[color]*n,
108 | 'position':position,
109 | 'rotator':(rot_max,rot_min,rate,0) }
110 | guid = allocator.add(polygon)
111 | return guid
112 |
113 | def add_regular_polygon(n_sides,radius,position,
114 | color=(.5,.5,.5),allocator=allocator):
115 | pts = polyOfN(n_sides,radius)
116 | poly_verts = wind_vertices(pts)
117 | n = len(poly_verts)
118 | polygon = {
119 | 'render_verts': [(x+position[0],y+position[1],position[2]) for x,y,_,_,_ in poly_verts],
120 | 'color':[color]*n,
121 | 'position':position,}
122 | guid = allocator.add(polygon)
123 | return guid
124 |
125 | def update_rotator(rotator):
126 | arr=rotator
127 | span = arr['max_val'] - arr['min_val']
128 | arr['accumulator'] += arr['interval']
129 | underflow = arr['accumulator'] <= arr['min_val']
130 | arr['accumulator'][underflow] += span[underflow]
131 | overflow = arr['accumulator'] >= arr['max_val']
132 | arr['accumulator'][overflow] -= span[overflow]
133 |
134 |
135 | def update_render_verts(render_verts,poly_verts,positions,rotator,indices=[]):
136 | '''Update vertices to render based on positions and angles communicated
137 | through the data accessors'''
138 |
139 | angles = rotator['accumulator']
140 |
141 | cos_ts, sin_ts = cos(angles), sin(angles)
142 | cos_ts -= 1
143 | #here's a mouthfull. see contruction of initial_data in init. sum-difference folrmula applied
144 | #and simplified. work it out on paper if you don't believe me.
145 | xs, ys, rs, xhelpers, yhelpers = (poly_verts[:,x] for x in range(5))
146 | pts = render_verts
147 |
148 | #print 'shapes:',angles.shape
149 | pts[:,0] = xhelpers*cos_ts[indices]
150 | pts[:,1] = yhelpers*sin_ts[indices]
151 | pts[:,0] -= pts[:,1]
152 | pts[:,0] *= rs
153 | pts[:,0] += xs
154 | pts[:,0] += positions[indices,0]
155 |
156 | pts[:,1] = yhelpers*cos_ts[indices]
157 | tmp = xhelpers*sin_ts[indices]
158 | pts[:,1] += tmp
159 | pts[:,1] *= rs
160 | pts[:,1] += ys
161 | pts[:,1] += positions[indices,1]
162 |
163 | def update_display(render_verts,colors):
164 | gl.glClearColor(0.2, 0.4, 0.5, 1.0)
165 | gl.glBlendFunc (gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
166 | gl.glEnable (gl.GL_BLEND)
167 | gl.glEnable (gl.GL_LINE_SMOOTH);
168 | gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
169 | gl.glEnableClientState(gl.GL_COLOR_ARRAY)
170 | gl.glEnable(gl.GL_DEPTH_TEST)
171 |
172 | n = len(render_verts[:])
173 | #TODO verts._buffer.ctypes.data is awkward
174 | gl.glVertexPointer(3, vert_dtype.gl, 0, render_verts[:].ctypes.data)
175 | gl.glColorPointer(3, color_dtype.gl, 0, colors[:].ctypes.data)
176 | gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, n)
177 |
178 |
179 |
180 |
181 | if __name__ == '__main__':
182 |
183 | width, height = 640,480
184 | window = pyglet.window.Window(width, height,vsync=False)
185 | #window = pyglet.window.Window(fullscreen=True,vsync=False)
186 | #width = window.width
187 | #height = window.height
188 | fps_display = pyglet.clock.ClockDisplay()
189 | text = """Numpy ECS"""
190 | label = pyglet.text.HTMLLabel(text, x=10, y=height-10)
191 |
192 | n1=1000
193 |
194 | positions = [(x*width,y*height,z) for x,y,z in np.random.random((n1,3))]
195 | rs = [r*50 for r in np.random.random(n1)]
196 | ns = [int(m*10)+3 for m in np.random.random(n1)]
197 | colors = np.random.random((n1,3)).astype(color_dtype.np)
198 | rates = np.random.random(n1)*.01
199 |
200 | for n,r, position, color, rate in zip(ns,rs, positions, colors, rates):
201 | add_rotating_regular_polygon(n,r,position,rate,color)
202 | #from before indicies could be calculated
203 | #indices = np.array(reduce(add, [[x,]*7 for x in range(n1)], []),dtype=np.int)
204 |
205 | n2=50
206 |
207 | positions = [(x*width,y*height,z) for x,y,z in np.random.random((n2,3))]
208 | rs = [r*50 for r in np.random.random(n2)]
209 | ns = [int(m*10)+3 for m in np.random.random(n2)]
210 | colors = np.random.random((n2,3)).astype(color_dtype.np)
211 |
212 | for n_sides,radius, position, color in zip(ns,rs, positions, colors):
213 | add_regular_polygon(n_sides,radius,position,color)
214 |
215 | allocator._defrag()
216 |
217 | # Create our hero
218 | # white because it stands out against the random colors
219 | # rotating because that's the system that updates the render_verts
220 | hero_guid = add_rotating_regular_polygon(6,75,(width/2,height/2,1),0,color=(1,1,1))
221 | allocator._defrag()
222 |
223 | # note that hero_guid does not exist until the allocator id defragged
224 | hero = allocator.accessor_from_guid(hero_guid)
225 |
226 |
227 | movement_vector = np.array([0,0],np.int)
228 |
229 | @window.event
230 | def on_key_press(symbol, modifiers):
231 | global movement_vector
232 | if symbol == UP:
233 | movement_vector[1] += 1
234 | elif symbol == DOWN:
235 | movement_vector[1] -= 1
236 | elif symbol == RIGHT:
237 | movement_vector[0] += 1
238 | elif symbol == LEFT:
239 | movement_vector[0] -= 1
240 |
241 | @window.event
242 | def on_key_release(symbol, modifiers):
243 | global movement_vector
244 | if symbol == UP:
245 | movement_vector[1] += -1
246 | elif symbol == DOWN:
247 | movement_vector[1] -= -1
248 | elif symbol == RIGHT:
249 | movement_vector[0] += -1
250 | elif symbol == LEFT:
251 | movement_vector[0] -= -1
252 |
253 |
254 | get_sections = allocator.selectors_from_component_query
255 | @window.event
256 | def on_draw():
257 | window.clear()
258 |
259 | hero.position[0][:2] += movement_vector
260 |
261 | rotator = ('rotator',)
262 | sections = get_sections(rotator)
263 | update_rotator(*(sections[name] for name in rotator))
264 |
265 | render_verts =('render_verts','poly_verts','position','rotator')
266 | broadcast = ('position__to__poly_verts',)
267 | sections = get_sections(render_verts + broadcast)
268 | indices = sections.pop(broadcast[0])
269 | update_render_verts(*(sections[name] for name in render_verts),indices=indices)
270 |
271 | draw =('render_verts','color')
272 | sections = get_sections(draw)
273 | update_display(*(sections[name] for name in draw))
274 |
275 | fps_display.draw()
276 |
277 | pyglet.clock.schedule(lambda _: None)
278 |
279 | pyglet.app.run()
280 |
--------------------------------------------------------------------------------
/examples/polygons.py:
--------------------------------------------------------------------------------
1 | '''
2 | Numpy-ECS example of rotating and rendering polygons
3 |
4 | Hard coded example of the following Entity_class_id table:
5 |
6 | entity_class_id, vertices, color, positions, rotator,
7 |
8 | 1110, 1, 1, 1, 0,
9 | 1111, 1, 1, 1, 1,
10 |
11 | This file is part of Numpy-ECS.
12 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com)
13 |
14 | Numpy-ECS is free software: you can redistribute it and/or modify
15 | it under the terms of the GNU General Public License as published by
16 | the Free Software Foundation, either version 2 of the License, or
17 | (at your option) any later version.
18 |
19 | Data Oriented Python is distributed in the hope that it will be useful,
20 | but WITHOUT ANY WARRANTY; without even the implied warranty of
21 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 | GNU General Public License for more details.
23 |
24 | You should have received a copy of the GNU General Public License
25 | along with this program. If not, see .
26 | '''
27 | import numpy as np
28 | from numpy import sin, cos, pi, sqrt
29 | from math import atan2
30 | import pyglet
31 | from pyglet import gl
32 | from collections import namedtuple
33 | from operator import add
34 |
35 | from numpy_ecs.global_allocator import GlobalAllocator
36 | from numpy_ecs.components import DefraggingArrayComponent as Component
37 |
38 | dtype_tuple = namedtuple('Dtype',('np','gl'))
39 | vert_dtype = dtype_tuple(np.float32,gl.GL_FLOAT)
40 | color_dtype = dtype_tuple(np.float32,gl.GL_FLOAT)
41 |
42 | counter_type = np.dtype([('max_val', np.float32),
43 | ('min_val', np.float32),
44 | ('interval', np.float32),
45 | ('accumulator',np.float32)])
46 |
47 |
48 | allocator = GlobalAllocator((Component('render_verts' , (3,), vert_dtype.np ),
49 | Component('poly_verts' , (5,), color_dtype.np),
50 | Component('color' , (3,), color_dtype.np),
51 | Component('position' , (3,), vert_dtype.np ),
52 | Component('rotator' , (1,), counter_type )),
53 |
54 | allocation_scheme = (
55 | (1,0,1,1,0),
56 | (1,1,1,1,1),
57 | )
58 | )
59 |
60 |
61 | #####################################
62 | #
63 | # Entity creation helper functions
64 | #
65 | ######################################
66 |
67 | def polyOfN(n,radius):
68 | '''helper function for making polygons'''
69 | r=radius
70 | if n < 3:
71 | n=3
72 | da = 2*pi/(n) #angle between divisions
73 | return [[r*cos(da*x),r*sin(da*x)] for x in range(int(n))]
74 |
75 | def wind_vertices(pts):
76 | l = len(pts)
77 | #wind up the vertices, so we don't have to do it when speed counts.
78 | #I dont really know which is clock wise and the other counter clock wise, btw
79 | cw = pts[:l//2]
80 | ccw = pts[l//2:][::-1]
81 | flatverts = [None]*(l)
82 | flatverts[::2]=ccw
83 | flatverts[1::2]=cw
84 | wound = [flatverts[0]]+flatverts+[flatverts[-1]]
85 | #prewound vertices can be transformed without care for winding.
86 | #Now, store the vertices in a way that can be translated as efficiently as possible later
87 | #construct list of (x,y,r, x_helper, y_helper)
88 | #note that from alpha to theta, x changes by r*[cos(theta+alpha)-cos(alpha)]
89 | #lets call the initial angle of a vert alpha
90 | #so later, at theta, we want to remember cos(alpha) and sin(alpha)
91 | #they are the helper values
92 | return [(pt[0],pt[1],sqrt(pt[0]**2+pt[1]**2),
93 | cos(atan2(pt[1],pt[0])),sin(atan2(pt[1],pt[0]))) for pt in wound]
94 |
95 | def add_rotating_regular_polygon(n_sides,radius,position,rate,
96 | color=(.5,.5,.5),allocator=allocator):
97 | rot_max = 4*np.pi
98 | rot_min = -rot_max
99 | pts = polyOfN(n_sides,radius)
100 | poly_verts = wind_vertices(pts)
101 | n = len(poly_verts)
102 | polygon = {
103 | 'poly_verts': poly_verts,
104 | 'render_verts': [(0,0,position[2])]*n,
105 | 'color':[color]*n,
106 | 'position':position,
107 | 'rotator':(rot_max,rot_min,rate,0) }
108 | allocator.add(polygon)
109 |
110 | def add_regular_polygon(n_sides,radius,position,
111 | color=(.5,.5,.5),allocator=allocator):
112 | pts = polyOfN(n_sides,radius)
113 | poly_verts = wind_vertices(pts)
114 | n = len(poly_verts)
115 | polygon = {
116 | 'render_verts': [(x+position[0],y+position[1],position[2]) for x,y,_,_,_ in poly_verts],
117 | 'color':[color]*n,
118 | 'position':position,}
119 | allocator.add(polygon)
120 |
121 |
122 |
123 | #############
124 | #
125 | # Systems
126 | #
127 | #############
128 |
129 | def update_rotator(rotator):
130 | arr=rotator
131 | span = arr['max_val'] - arr['min_val']
132 | arr['accumulator'] += arr['interval']
133 | underflow = arr['accumulator'] <= arr['min_val']
134 | arr['accumulator'][underflow] += span[underflow]
135 | overflow = arr['accumulator'] >= arr['max_val']
136 | arr['accumulator'][overflow] -= span[overflow]
137 |
138 |
139 | def update_render_verts(render_verts,poly_verts,positions,rotator,indices=[]):
140 | '''Update vertices to render based on positions and angles communicated
141 | through the data accessors'''
142 |
143 | angles = rotator['accumulator']
144 |
145 | cos_ts, sin_ts = cos(angles), sin(angles)
146 | cos_ts -= 1
147 | #here's a mouthfull. see contruction of initial_data in init. sum-difference folrmula applied
148 | #and simplified. work it out on paper if you don't believe me.
149 | xs, ys, rs, xhelpers, yhelpers = (poly_verts[:,x] for x in range(5))
150 | pts = render_verts
151 |
152 | #print 'shapes:',angles.shape
153 | pts[:,0] = xhelpers*cos_ts[indices]
154 | pts[:,1] = yhelpers*sin_ts[indices]
155 | pts[:,0] -= pts[:,1]
156 | pts[:,0] *= rs
157 | pts[:,0] += xs
158 | pts[:,0] += positions[indices,0]
159 |
160 | pts[:,1] = yhelpers*cos_ts[indices]
161 | tmp = xhelpers*sin_ts[indices]
162 | pts[:,1] += tmp
163 | pts[:,1] *= rs
164 | pts[:,1] += ys
165 | pts[:,1] += positions[indices,1]
166 |
167 | def update_display(render_verts,colors):
168 | gl.glClearColor(0.2, 0.4, 0.5, 1.0)
169 | gl.glBlendFunc (gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
170 | gl.glEnable (gl.GL_BLEND)
171 | gl.glEnable (gl.GL_LINE_SMOOTH);
172 | gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
173 | gl.glEnableClientState(gl.GL_COLOR_ARRAY)
174 |
175 | n = len(render_verts[:])
176 | #TODO verts._buffer.ctypes.data is awkward
177 | gl.glVertexPointer(3, vert_dtype.gl, 0, render_verts[:].ctypes.data)
178 | gl.glColorPointer(3, color_dtype.gl, 0, colors[:].ctypes.data)
179 | gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, n)
180 |
181 |
182 | if __name__ == '__main__':
183 |
184 | width, height = 640,480
185 | window = pyglet.window.Window(width, height,vsync=False)
186 | #window = pyglet.window.Window(fullscreen=True,vsync=False)
187 | #width = window.width
188 | #height = window.height
189 | fps_display = pyglet.clock.ClockDisplay()
190 | text = """Numpy ECS"""
191 | label = pyglet.text.HTMLLabel(text, x=10, y=height-10)
192 |
193 | n1=1000
194 |
195 | positions = [(x*width,y*height,z) for x,y,z in np.random.random((n1,3))]
196 | rs = [r*50 for r in np.random.random(n1)]
197 | ns = [int(m*10)+3 for m in np.random.random(n1)]
198 | colors = np.random.random((n1,3)).astype(color_dtype.np)
199 | rates = np.random.random(n1)*.01
200 |
201 | for n,r, position, color, rate in zip(ns,rs, positions, colors, rates):
202 | add_rotating_regular_polygon(n,r,position,rate,color)
203 | #from before indicies could be calculated
204 | #indices = np.array(reduce(add, [[x,]*7 for x in range(n1)], []),dtype=np.int)
205 |
206 | n2=50
207 |
208 | positions = [(x*width,y*height,z) for x,y,z in np.random.random((n2,3))]
209 | rs = [r*50 for r in np.random.random(n2)]
210 | ns = [int(m*10)+3 for m in np.random.random(n2)]
211 | colors = np.random.random((n2,3)).astype(color_dtype.np)
212 |
213 | for n_sides,radius, position, color in zip(ns,rs, positions, colors):
214 | add_regular_polygon(n_sides,radius,position,color)
215 |
216 | allocator._defrag()
217 |
218 | get_sections = allocator.selectors_from_component_query
219 |
220 | @window.event
221 | def on_draw():
222 | window.clear()
223 |
224 | rotator = ('rotator',)
225 | sections = get_sections(rotator)
226 | update_rotator(*(sections[name] for name in rotator))
227 |
228 | render_verts =('render_verts','poly_verts','position','rotator')
229 | broadcast = ('position__to__poly_verts',)
230 | sections = get_sections(render_verts + broadcast)
231 | indices = sections.pop(broadcast[0])
232 | update_render_verts(*(sections[name] for name in render_verts),indices=indices)
233 |
234 | draw =('render_verts','color')
235 | sections = get_sections(draw)
236 | update_display(*(sections[name] for name in draw))
237 |
238 | fps_display.draw()
239 |
240 | pyglet.clock.schedule(lambda _: None)
241 |
242 | pyglet.app.run()
243 |
244 |
245 |
--------------------------------------------------------------------------------
/examples/robin_animation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Permafacture/data-oriented-pyglet/b0d54a8e0e25aec55b77c4df65b0e0c0d6940280/examples/robin_animation.png
--------------------------------------------------------------------------------
/examples/second.py:
--------------------------------------------------------------------------------
1 | '''
2 | First example using rough data orientation
3 |
4 | Although the same object oriented style is used for generating the data,
5 | it is aggregated together so that heavy maths can be applied en masse.
6 |
7 | This sloppy data aggregation comes at the price of being able to access
8 | individual shapes post creation. IE, all polygons have the same number
9 | of sides and turn at the same rate.
10 |
11 | This file is part of Numpy-ECS.
12 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com)
13 |
14 | Numpy-ECS Python is free software: you can redistribute it and/or modify
15 | it under the terms of the GNU General Public License as published by
16 | the Free Software Foundation, either version 2 of the License, or
17 | (at your option) any later version.
18 |
19 | Data Oriented Python is distributed in the hope that it will be useful,
20 | but WITHOUT ANY WARRANTY; without even the implied warranty of
21 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 | GNU General Public License for more details.
23 |
24 | You should have received a copy of the GNU General Public License
25 | along with this program. If not, see .
26 |
27 |
28 | '''
29 |
30 | from __future__ import absolute_import, division, print_function, unicode_literals
31 | import numpy as np
32 | import pyglet
33 | from pyglet import gl
34 | from math import pi, sin, cos,atan2,sqrt
35 | import time
36 | from collections import namedtuple
37 |
38 |
39 | #Keep datatypes between numpy and gl consistent
40 | dtype_tuple = namedtuple('Dtype',('np_type','gl_type'))
41 | vert_dtype = dtype_tuple(np.float32,gl.GL_FLOAT)
42 | color_dtype = dtype_tuple(np.float32,gl.GL_FLOAT)
43 |
44 | #Limit run time for profiling
45 | run_for = 15 #seconds to run test for
46 | def done_yet(duration = run_for, start=time.time()):
47 | return time.time()-start > duration
48 |
49 |
50 | #Set up window
51 | width, height = 640,480
52 | window = pyglet.window.Window(width, height,vsync=False)
53 | #window = pyglet.window.Window(fullscreen=True,vsync=False)
54 | #width = window.width
55 | #height = window.height
56 | fps_display = pyglet.clock.ClockDisplay()
57 | text = """Optimized DOP"""
58 | label = pyglet.text.HTMLLabel(text, x=10, y=height-10)
59 |
60 |
61 | def draw(verts,colors):
62 | '''draw the numpy arrays `verts` and `colors`.'''
63 |
64 | gl.glClearColor(0.2, 0.4, 0.5, 1.0)
65 | gl.glBlendFunc (gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
66 | gl.glEnable (gl.GL_BLEND)
67 | gl.glEnable (gl.GL_LINE_SMOOTH);
68 | gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
69 | gl.glEnableClientState(gl.GL_COLOR_ARRAY)
70 |
71 | gl.glVertexPointer(2, vert_dtype.gl_type, 0, verts.ctypes.data)
72 | gl.glColorPointer(3, color_dtype.gl_type, 0, colors.ctypes.data)
73 | gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(verts)//2)
74 | fps_display.draw()
75 |
76 | #helper hunction for making polygons
77 | def polyOfN(radius,n):
78 | r=radius
79 | if n < 3:
80 | n=3
81 | da = 2*pi/(n) #angle between divisions
82 | return [[r*cos(da*x),r*sin(da*x)] for x in range(n)]
83 |
84 |
85 | class Convex(object):
86 | '''Convex polygons for rendering'''
87 | def __init__(self,pts, position=None, color=None, radius=0):
88 | if position is None:
89 | position = (width//2, height//2)
90 | self.position = position
91 | if color is None:
92 | color = [60,0,0]
93 | self.color = tuple(color)
94 | self.pts=pts
95 | self.initializeVertices()
96 |
97 | def initializeVertices(self):
98 | px,py = self.position
99 | pts = self.pts
100 | l = len(pts)
101 | #wind up the vertices, so we don't have to do it when speed counts.
102 | #I dont really know which is clock wise and the other counter clock wise, btw
103 | cw = pts[:l//2]
104 | ccw = pts[l//2:][::-1]
105 | flatverts = [None]*(l)
106 | flatverts[::2]=ccw
107 | flatverts[1::2]=cw
108 | wound = [flatverts[0]]+flatverts+[flatverts[-1]]
109 | #prewound vertices can be transformed without care for winding.
110 | #Now, store the vertices in a way that can be translated as efficiently as possible later
111 | #construct list of (x,y,r, x_helper, y_helper)
112 | #note that from alpha to theta, x changes by r*[cos(theta+alpha)-cos(alpha)]
113 | #lets call the initial angle of a vert alpha
114 | #so later, at theta, we want to remember cos(alpha) and sin(alpha)
115 | #they are the helper values
116 | self.initial_data = [(px,py,pt[0],pt[1],sqrt(pt[0]**2+pt[1]**2),
117 | cos(atan2(pt[1],pt[0])),sin(atan2(pt[1],pt[0]))) for pt in wound]
118 | self.n = len(self.initial_data)
119 | self.set_colors()
120 |
121 | def set_colors(self):
122 | self.colors = self.color*self.n
123 |
124 |
125 |
126 |
127 | ## Create shapes
128 | n=150
129 | size = 5 #number of sides
130 | positions = [(x*width,y*height) for x,y in np.random.random((n,2))]
131 | poly_args = [(r*50,size) for r,m in np.random.random((n,2))]
132 | colors = np.random.random((n,3)).astype(color_dtype.np_type)
133 |
134 | ents = [Convex(polyOfN(*pargs),position=pos, color=col) for pargs,pos,col in zip(poly_args,positions,colors)]
135 |
136 |
137 | #Create Data Oriented Arrays
138 | helpers = []
139 | colors = []
140 | for ent in ents:
141 | helpers.extend(ent.initial_data)
142 | colors.extend(ent.colors)
143 |
144 | helpers = np.array(helpers)
145 | colors = np.array(colors,dtype=color_dtype.np_type)
146 |
147 | def mass_rotate(initial_data,theta):
148 | initiald = initial_data
149 | cost, sint = cos(theta), sin(theta)
150 | #here's a mouthfull. see contruction of initial_data in init. sum-difference folrmula applied
151 | #and simplified. work it out on paper if you don't believe me.
152 | pxs, pys, xs, ys, rs, xhelpers, yhelpers = (initiald[:,x] for x in range(7))
153 |
154 | pts = np.empty((len(pxs),2),dtype=vert_dtype.np_type)
155 |
156 | pts[:,0] = xhelpers*(cost-1)
157 | pts[:,1] = yhelpers*sint
158 | pts[:,0] -= pts[:,1]
159 | pts[:,0] *= rs
160 | pts[:,0] += xs
161 | pts[:,0] += pxs
162 |
163 | pts[:,1] = yhelpers*(cost-1)
164 | tmp = xhelpers*sint
165 | pts[:,1] += tmp
166 | pts[:,1] *= rs
167 | pts[:,1] += ys
168 | pts[:,1] += pys
169 |
170 | #flatten and return as correct type
171 | pts.shape = ( reduce(lambda xx,yy: xx*yy, pts.shape), )
172 | return pts.astype(vert_dtype.np_type)
173 |
174 |
175 | #instatiate verts array
176 | verts = mass_rotate(helpers,0)
177 |
178 | #global state for polygon rotations
179 | angle = 0
180 | rate = .002
181 |
182 | @window.event
183 | def on_draw():
184 | global angle,verts,colors
185 |
186 | if done_yet():
187 | pyglet.app.exit()
188 |
189 | window.clear()
190 | angle+=rate
191 | verts[:] = mass_rotate(helpers,angle)
192 | draw(verts,colors)
193 |
194 |
195 |
196 | #pyglet.clock.set_fps_limit(60)
197 | pyglet.clock.schedule(lambda _: None)
198 |
199 | pyglet.app.run()
200 |
201 |
202 |
--------------------------------------------------------------------------------
/notes.txt:
--------------------------------------------------------------------------------
1 |
2 | ##Buffer
3 |
4 | domain_map = self.group_map[group]
5 |
6 | key = (formats, mode, indexed)
7 | try:
8 | domain = domain_map[key]
9 | except KeyError:
10 | # Create domain
11 |
12 | A domain only knows its formats (and wether it is indexed or not by virtue of which type it is an instance of)
13 | modes are kept seperate through the key
14 |
15 | Formats are like `v3f/stream` that get parsed into attributes within the domain
16 |
17 | ##Domain
18 |
19 | instantiated with list of tuples (AbstractAttribute, usage(static/dynamic/etc), and vob(True/False)
20 |
21 | Domains have an alloc and dealloc through their allocator
22 |
23 | Each attribute is assigned a buffer, which is a vertexbuffer.MappableVertexBufferObject if
24 | possible (easily possible) and desired, or a vertexbuffer.VertexArray.
25 |
26 | (MappableVertexBufferObject has a system-memory backed store, while VertexArray does not.)
27 |
28 | These seperate attribute buffers are sized to all contain the same number of elements:
29 | size of ctype * number of dimensions in vector * allocator.capacity
30 |
31 | Domain stores these as domain.buffer_attributes = [(this_buffer, attribute)], and also in the dict:
32 | attribute.names, keyed by the attribute name string
33 |
34 |
35 | ## VertexList
36 | A vertex list gets its colors, vertices, etc through setters and getters by getting that
37 | attribute's buffer from it's domain and using attribute.get_region with it's own start and count.
38 |
39 | get_region gets data like:
40 |
41 | '''
42 | notes from __init__
43 | self.count = count #assert count in (1, 2, 3, 4), 'Component count out of range'
44 | self.align = ctypes.sizeof(self.c_type)
45 | self.size = count * self.align
46 | self.stride = self.size #probably different for interleaved...
47 | self.offset = 0
48 | '''
49 |
50 |
51 | byte_start = self.stride * start
52 | byte_size = self.stride * count
53 | array_count = self.count * count # num of elements per vector * number of vectors requested == num of ctypes to get
54 | if self.stride == self.size or not array_count:
55 | # non-interleaved
56 | ptr_type = ctypes.POINTER(self.c_type * array_count)
57 | return buffer.get_region(byte_start, byte_size, ptr_type)
58 | else:
59 | # interleaved
60 | # Elliot: more complicated. Comments in VertexDomain __init__ lead me to assume this is only for static attributes.
61 |
62 |
63 |
--------------------------------------------------------------------------------
/numpy_ecs/__init__.py:
--------------------------------------------------------------------------------
1 | '''
2 | This file is part of Numpy-ECS.
3 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com)
4 |
5 | Numpy-ECS is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU General Public License as published by
7 | the Free Software Foundation, either version 2 of the License, or
8 | (at your option) any later version.
9 |
10 | Data Oriented Python is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU General Public License for more details.
14 |
15 | You should have received a copy of the GNU General Public License
16 | along with this program. If not, see .
17 | '''
18 |
--------------------------------------------------------------------------------
/numpy_ecs/accessors.py:
--------------------------------------------------------------------------------
1 | '''
2 | Accessors provide an object oriented interface for a specific
3 | entity instance (guid) by providing an object with attributes
4 | that access the array slices allocated to that guid under
5 | the hood
6 |
7 | This file is part of Numpy-ECS.
8 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com)
9 |
10 | Numpy-ECS is free software: you can redistribute it and/or modify
11 | it under the terms of the GNU General Public License as published by
12 | the Free Software Foundation, either version 2 of the License, or
13 | (at your option) any later version.
14 |
15 | Data Oriented Python is distributed in the hope that it will be useful,
16 | but WITHOUT ANY WARRANTY; without even the implied warranty of
17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 | GNU General Public License for more details.
19 |
20 | You should have received a copy of the GNU General Public License
21 | along with this program. If not, see .
22 | '''
23 | from __future__ import absolute_import, division, print_function
24 | import sys
25 | #python version compatability
26 | if sys.version_info < (3,0):
27 | from future_builtins import zip, map
28 |
29 | import numpy as np
30 |
31 | def build_references(allocator,guid):
32 | '''returns {component_name: slice or None, ...} for guid'''
33 | component_names = allocator.names
34 | get_slice = allocator._allocation_table.slices_from_guid
35 | slices = (s if s.start != s.stop else None for s in get_slice(guid))
36 | return dict(zip(component_names,slices))
37 |
38 | #TODO, I would like for methods of Accessor only called by Accessor to
39 | # be double underscored, but since they are used in the Factory, the
40 | # double underscore gets expanded to be the Factory. There might be
41 | # too much meta to make them private to a class that is created
42 | # just in time.
43 |
44 | class Accessor(object):
45 | '''
46 | Accessors provide an object oriented interface for a specific
47 | entity instance (guid) by providing an object with attributes
48 | that access the array slices allocated to that guid under
49 | the hood.'''
50 |
51 | def __init__(self,guid):
52 | # self._allocator is provided by the factory
53 | self._guid = guid
54 | self._dirty = True #do slices need to be re-calculated?
55 | self._active = True #is this accessor deleteable?
56 | self._references = {} #{component_name:slice,...}
57 | #The factory gave us a property for every component in the Table
58 | # but any specific guid won't have values for all of those
59 | # so we delete them
60 | # TODO can't delete a class property from an instance :'(
61 | #for name,val in build_references(self._allocator,guid).items():
62 | # if val is None:
63 | # print("delete {}".format(name))
64 | # print(dir(self))
65 | # delattr(self,name)
66 |
67 | def _rebuild_references(self,build_ref = build_references):
68 | '''Get the up to date slices for all the attributes'''
69 | # the is not None is not necessary, but not harmful
70 | self._references = {k:v for k,v in build_ref(self._allocator,
71 | self._guid).items() if v is not None}
72 | self._dirty = False
73 |
74 | #def resize(self,new_size):
75 | # self._domain.safe_realloc(self._id,new_size)
76 |
77 | #def close(self):
78 | # self.__closed = True
79 | # self._domain.safe_dealloc(self._id)
80 |
81 | #def __del__(self):
82 | # if not self.__closed:
83 | # self.close()
84 |
85 | def __repr__(self):
86 | return ""%(self._guid,)
87 |
88 |
89 | class AccessorFactory(object):
90 |
91 | def __init__(self,allocator):
92 | self.allocator=allocator
93 |
94 | def attribute_getter_factory(self, component_name):
95 | '''generate a getter for this component_name into the Component data array'''
96 | comp_dict = self.allocator.component_dict
97 | def getter(accessor, name=component_name, comp_dict=comp_dict):
98 | if accessor._dirty:
99 | accessor._rebuild_references()
100 | selector = accessor._references[name]
101 | return comp_dict[name][selector]
102 | return getter
103 |
104 | def attribute_setter_factory(self, component_name):
105 | '''generate a setter using this object's index to the domain arrays
106 | attr is the domain's list of this attribute'''
107 | comp_dict = self.allocator.component_dict
108 | def setter(accessor, data, name=component_name,comp_dict=comp_dict):
109 | if accessor._dirty:
110 | accessor._rebuild_references()
111 | selector = accessor._references[name]
112 | comp_dict[name][selector] = data
113 | return setter
114 |
115 | def generate_accessor(self):
116 | '''return a DataAccessor class that can be instatiated with a
117 | guid to provide an object oriented interface with the data
118 | associated with that guid'''
119 | NewAccessor = type('DataAccessor',(Accessor,),{})
120 |
121 | allocator = self.allocator
122 | getter = self.attribute_getter_factory
123 | setter = self.attribute_setter_factory
124 | comp_dict = allocator.component_dict
125 | for name in allocator.names:
126 | print("adding property: {}".format(name))
127 | setattr(NewAccessor,name,property(getter(name), setter(name)))
128 | NewAccessor._allocator = self.allocator
129 | return NewAccessor
130 |
131 |
132 |
--------------------------------------------------------------------------------
/numpy_ecs/components.py:
--------------------------------------------------------------------------------
1 | '''
2 | Class that wraps a numpy array as a buffer, and provides for resizing,
3 | inserting, appending, and defragging.
4 |
5 | This file is part of Numpy-ECS.
6 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com)
7 |
8 | Numpy-ECS is free software: you can redistribute it and/or modify
9 | it under the terms of the GNU General Public License as published by
10 | the Free Software Foundation, either version 2 of the License, or
11 | (at your option) any later version.
12 |
13 | Data Oriented Python is distributed in the hope that it will be useful,
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | GNU General Public License for more details.
17 |
18 | You should have received a copy of the GNU General Public License
19 | along with this program. If not, see .
20 | '''
21 | import numpy as np
22 |
23 | def _nearest_pow2(v):
24 | # From http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
25 | # Credit: Sean Anderson
26 | v -= 1
27 | v |= v >> 1
28 | v |= v >> 2
29 | v |= v >> 4
30 | v |= v >> 8
31 | v |= v >> 16
32 | return v + 1
33 |
34 | class DefraggingArrayComponent(object):
35 | '''holds a resize-able, re-allocateable, numpy array buffer'''
36 |
37 | def __init__(self,name,dim,dtype,size=0):
38 | ''' create a numpy array buffer of shape (size,dim) with dtype==dtype'''
39 | #TODO: might could alternatively instatiate with an existing numpy array?
40 | self.name = name
41 | self.datatype=dtype #calling this dtype would be confusing because this is not a numpy array!
42 | self._dim = dim
43 | self.capacity = size
44 | if dim == (1,):
45 | self._buffer = np.empty(size,dtype=dtype) #shape = (size,) not (size,1)
46 | self.resize = self._resize_singledim
47 | elif dim > 1:
48 | self._buffer = np.empty((size,)+dim,dtype=dtype) #shape = (size,dim)
49 | self.resize = self._resize_multidim
50 | else:
51 | raise ValueError('''ArrayComponent dim must be >= 1''')
52 |
53 | #def resize(self,count):
54 | # '''resize this component to new size = count'''
55 | # #This is a placeholder, __init__ decides what to overwrite this with
56 | # # ^ not true any more
57 | # self._buffer= self._buffer.resize((count,)+self._dim)
58 |
59 |
60 | def assert_capacity(self,new_capacity):
61 | '''make certain Component is atleast `new_capcpity` big.
62 | resizing if necessary.'''
63 | if self.capacity < new_capacity:
64 | #print "change capacity:",self.capacity,"->", new_capacity
65 | self.resize(_nearest_pow2(new_capacity))
66 | self.capacity = new_capacity
67 |
68 | def realloc(self,old_selector,new_selector):
69 | self._buffer[new_selector] = self._buffer[old_selector]
70 |
71 | #def push_from_index(self,index,size):
72 | # '''push all data in buffer from start onward forward by size.
73 | # if size is negative, moves everything backwards. Assumes alloc
74 | # was already called, assuring that there is size empty values at
75 | # end of buffer.'''
76 | # end = self.capacity
77 | # if size == 0:
78 | # return
79 | # elif size > 0:
80 | # self._buffer[index+size:] = self._buffer[index:-size]
81 | # elif size < 0:
82 | # self._buffer[index+size:size] = self._buffer[index:]
83 |
84 |
85 | def __getitem__(self,selector):
86 | #assert self.datatype == self._buffer.dtype #bug if numpy changes dtype
87 | return self._buffer[selector]
88 |
89 | def __setitem__(self,selector,data):
90 | self._buffer[selector]=data
91 | assert self.datatype == self._buffer.dtype, 'numpy dtype may not change'
92 |
93 | def _resize_multidim(self,count):
94 | shape =(count,)+self._dim
95 | try:
96 | #this try is because empty record arrays cannot be resized.
97 | #should be fixed in a more recent numpy.
98 | self._buffer.resize(shape)
99 | except ValueError:
100 | self._buffer = np.resize(self._buffer,shape)
101 |
102 | def _resize_singledim(self,count):
103 | try:
104 | #this try is because empty record arrays cannot be resized.
105 | #should be fixed in a more recent numpy.
106 | self._buffer.resize(count)
107 | except ValueError:
108 | self._buffer = np.resize(self._buffer,count)
109 |
110 | def __repr__(self):
111 | return ""%self.name
112 |
113 |
--------------------------------------------------------------------------------
/numpy_ecs/global_allocator.py:
--------------------------------------------------------------------------------
1 | '''
2 | Allocating Entity values in contiguous Component arrays, providing Systems
3 | with access to simple slices of components.
4 |
5 | This file is part of Numpy-ECS.
6 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com)
7 |
8 | Numpy-ECS is free software: you can redistribute it and/or modify
9 | it under the terms of the GNU General Public License as published by
10 | the Free Software Foundation, either version 2 of the License, or
11 | (at your option) any later version.
12 |
13 | Data Oriented Python is distributed in the hope that it will be useful,
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | GNU General Public License for more details.
17 |
18 | You should have received a copy of the GNU General Public License
19 | along with this program. If not, see .
20 | '''
21 |
22 | # Systems require slices, not index arrays or multiple slices, to access data
23 | # in components. Naively allocating spaces in arrays without regard for this
24 | # will not work. For instance:
25 | #
26 | # [
27 | #
28 | # entity_class_id, vertices, animated, positions, velocity
29 | #
30 | # 01, 1, 0, 0, 0
31 | # 02, 1, 0, 1, 0
32 | # 03, 1, 0, 1, 1
33 | # 04, 1, 1, 0, 0
34 | # 05, 1, 1, 1, 0
35 | # 06, 1, 1, 1, 1
36 | #
37 | # ]
38 | #
39 | # Trying to apply update_verts(positions, velocities), one cannot get a single
40 | # slice on positions. This table can be re-arranged to work:
41 | #
42 | # [
43 | #
44 | # entity_class_id, vertices, animated, positions, velocity
45 | #
46 | # 01, 1, 0, 0, 0
47 | # 02, 1, 0, 1, 0
48 | # 03, 1, 0, 1, 1
49 | # 06, 1, 1, 1, 1
50 | # 05, 1, 1, 1, 0
51 | # 04, 1, 1, 0, 0
52 | # ]
53 | #
54 | # Now velocities can update a single slice of positions, and postions can
55 | # update a single slice of vertices (including those that are also animated).
56 | #
57 | # If the entity_class_id is a binary representation of the components it
58 | # requires, then given an addition of any arbitrary collection of
59 | # attributes, we can determine what entity_class it belongs to and add
60 | # the values to sections of the array that maintain ideal contiguity.
61 | #
62 | # [
63 | #
64 | # entity_class_id, vertices, animated, positions, velocity
65 | #
66 | # 1000, 1, 0, 0, 0
67 | # 1010, 1, 0, 1, 0
68 | # 1011, 1, 0, 1, 1
69 | # 1111, 1, 1, 1, 1
70 | # 1110, 1, 1, 1, 0
71 | # 1100, 1, 1, 0, 0
72 | # ]
73 | #
74 | #
75 | from table import Table, INDEX_SEPERATOR
76 | import numpy as np
77 | from .table import Table, INDEX_SEPERATOR
78 | from .accessors import AccessorFactory
79 |
80 | def verify_component_schema(allocation_schema):
81 | '''given an allocation schema as a list of lists, return True if the schema
82 | keeps all Component arrays contiguous. Else return False'''
83 | started = [False] * len(allocation_schema[0])
84 | ended = [False] * len(allocation_schema[0])
85 | for row in allocation_schema:
86 | for i,val in enumerate(row):
87 | if val:
88 | if not started[i]:
89 | started[i] = True
90 | else:
91 | if ended[i]:
92 | return False
93 | elif started[i]:
94 | ended[i] = True
95 | return True
96 |
97 |
98 | class GlobalAllocator(object):
99 | '''Decides how data is added or removed from Components, allocating and
100 | deallocting Entities/guids. Components better be allocated by only one
101 | of these.
102 |
103 | see comments at top of file for details'''
104 |
105 | def __init__(self,components,allocation_scheme):
106 | self.component_dict = {component.name:component for component in components}
107 | self._memoized = {} #for selectors_from_component_names
108 |
109 | names = tuple(comp.name for comp in components)
110 | self.names = names
111 | self._allocation_table = Table(names, tuple(allocation_scheme))
112 |
113 | self._cached_adds = list()
114 | self._next_guid = 0
115 | self.__accessor_factory = AccessorFactory(self).generate_accessor()
116 | self.__accessors = [] #accessors that have been handed out
117 |
118 | def accessor_from_guid(self,guid):
119 | accessor = self.__accessor_factory(guid)
120 | self.__accessors.append(accessor)
121 | return accessor
122 |
123 | @property
124 | def next_guid(self):
125 | self._next_guid += 1
126 | return self._next_guid
127 |
128 | @property
129 | def guids(self):
130 | return tuple(self._allocation_table.guids)
131 | #def _class_id_from_guid(guid):
132 | # #assumes entity has non-zero size for every component that
133 | # # defines it's class. This is the definition of a class_id and is
134 | # # a result of how add works
135 | # row = row_for_guid(guid)
136 | # return int(''.join(x!=0 for x in row),base=2)
137 |
138 |
139 | #def allocation_schema_from_truth_table(self,truth_table):
140 | # '''
141 | # an allocation schema may be defined as a list of lists.
142 | #
143 | # convert that to a list of integers where (0,0,1,1) -> 3, etc'''
144 | # return map(self.entity_class_from_tuple,truth_table)
145 |
146 | #def entity_class_from_dict(self,component_dict):
147 | # '''takes a component dict like {'comp_name1':value}
148 |
149 | # returns the integer that is this entity class'''
150 | # names = self._allocation_table.column_names
151 | # for name in component_dict.keys():
152 | # assert name in names, "%s is not a known component name"% (name,)
153 | # entity_tuple = tuple(0 if (component_dict.get(name,None) is None) \
154 | # else 1 for name in names)
155 | # return self.entity_class_from_tuple(entity_tuple)
156 |
157 | def add(self,values_dict,guid=None):
158 | component_dict = self.component_dict
159 |
160 | def convert_to_component_array(component,value):
161 | '''converts value to a numpy array with appropriate shape for
162 | component'''
163 | value = np.array(value,dtype = component.datatype)
164 |
165 | shape = value.shape or (1,)
166 | dim = component._dim
167 | assert shape == dim or (len(shape)>1 and shape[1:] == dim), \
168 | "component '%s' expected shape %s, but got %s" % (
169 | component.name,component._dim,value.shape)
170 | #len(shape) is a look before I leap. No exceptions please
171 | return value
172 |
173 | result = {'guid': guid or self.next_guid}
174 | for name, value in values_dict.items():
175 | result[name] = convert_to_component_array(component_dict[name],value)
176 | assert result['guid'] not in {d['guid'] for d in self._cached_adds}, \
177 | "cannot add a guid twice"
178 | self._cached_adds.append(result)
179 | return result['guid']
180 |
181 | def delete(self,guid):
182 | alloc_table = self._allocation_table
183 | alloc_table.stage_delete(guid)
184 |
185 | def _defrag(self):
186 | alloc_table = self._allocation_table
187 | component_dict = self.component_dict
188 | adds_dict = self._cached_adds
189 | if (not adds_dict) and (None not in alloc_table.guids):
190 | return #nothing to do
191 |
192 | #delete_set = self._cached_deletes
193 | def safe_len(item):
194 | if item is None:
195 | return 0
196 | shape = item.shape
197 | if len(shape) > 1:
198 | #TODO only supports arrays like [n] and [1,n] !!!
199 | #TODO Can't think of fix or what one would be expecting but I
200 | #TODO must note this limitation
201 | return shape[0]
202 | else:
203 | return 1
204 |
205 | for add in self._cached_adds:
206 | guid = add['guid']
207 | add = tuple(safe_len(add.get(name,None)) \
208 | for name in alloc_table.column_names)
209 | alloc_table.stage_add(guid,add)
210 |
211 | #defrag
212 | #print "defrag"
213 | for name, (new_size, sources, targets) in zip(alloc_table.column_names,alloc_table.compress()):
214 | #if name == 'component_1': print "working on component_1"
215 | component = component_dict[name]
216 | component.assert_capacity(new_size)
217 | for source,target in zip(sources,targets):
218 | #if name == "component_1":
219 | # print "moving",component[source],"to",target
220 | component[target] = component[source]
221 |
222 | #apply adds
223 | for add in self._cached_adds:
224 | guid = add['guid']
225 | for name, this_slice in zip(alloc_table.column_names,alloc_table.slices_from_guid(guid)):
226 | if name in add:
227 | component_dict[name][this_slice] = add[name]
228 |
229 | #reset
230 | self._cached_adds = list()
231 | self._memoized = dict()
232 | for accessor in self.__accessors:
233 | accessor._dirty = True
234 |
235 | def is_valid_query(self,query,sep=INDEX_SEPERATOR):
236 | known_names = self.names
237 | for x in query:
238 | if (sep not in x) and (x not in known_names):
239 | print "%s in query is not valid"%x
240 | return False
241 | return True
242 |
243 | def selectors_from_component_query(self,query,sep=INDEX_SEPERATOR):
244 | #TODO add indicies to doc string
245 | '''takes: ['comp name 1', 'comp name 3', ...] #list tuple or set
246 | returns {'component name 1': component1[selector] ...}
247 | where selector is for the section where all components
248 | are defined'''
249 | assert isinstance(query,tuple), 'argument must be hashable'
250 | known_names = self.names
251 | assert self.is_valid_query(query), \
252 | 'col_names must be valid component names and index names'
253 | cache = self._memoized
254 | if query not in cache:
255 | indices = tuple(filter(lambda x: sep in x, query))
256 | col_names = tuple(filter(lambda x: x not in indices, query))
257 | table = self._allocation_table
258 | selectors, indices = table.mask_slices(col_names,indices)
259 | cdict = self.component_dict
260 | result = {n:cdict[n][s] for n,s in selectors.items()}
261 | result.update({n:np.array(lst) for n,lst in indices.items()})
262 | cache[query] = result
263 | else:
264 | result = cache[query]
265 | return dict(result) #return copy of cached result
266 |
267 |
268 |
269 |
270 | #########
271 | #
272 | # Components
273 | #
274 | #########
275 |
276 |
277 |
278 | if __name__ == '__main__':
279 | from components import DefraggingArrayComponent as Component
280 | import numpy as np
281 |
282 | #TODO tests should include: when there is nothing in the first class,
283 | # when there is nothing in the last class. When there is nothing
284 | # in one or many in between classes. When the allocation scheme has repeat
285 | # entries. When the allocation scheme is not contiguous. component
286 | # dimensions of (1,),(n,),(n,m) and when incorrect dimensions are added.
287 | # also, when components have complex data types (structs)
288 |
289 | d1 = Component('component_1',(1,),np.int32)
290 | d2 = Component('component_2',(2,),np.int32)
291 | d3 = Component('component_3',(3,),np.int32)
292 | allocation_scheme = (
293 | (1,1,1),
294 | (1,0,1),
295 | (1,0,0),)
296 |
297 | allocator = GlobalAllocator([d1,d2,d3],allocation_scheme)
298 |
299 | to_add = []
300 | to_add.append({'component_1':1,'component_3':(1,2,3),'component_2':((1,10),(2,20)),})
301 | to_add.append({'component_1':2,'component_3':(4,5,6),'component_2':((3,30),(4,40)),})
302 | to_add.append({'component_1':3,'component_3':(7,8,9),'component_2':((5,50),(6,60)),})
303 | to_add.append({'component_1':4,'component_3':(10,11,12),'component_2':((7,70),(8,80)),})
304 | to_add.append({'component_1':7,'component_3':(19,20,21),})
305 | trash = map(allocator.add,to_add)
306 | #allocator.add(to_add2)
307 | allocator._defrag()
308 | #print "d1:",d1[:]
309 |
310 | to_add = []
311 | to_add.append({'component_1':8,'component_3':(22,23,24),})
312 | trash = map(allocator.add,to_add)
313 | #allocator.add(to_add2)
314 | allocator._defrag()
315 | #print "d1:",d1[:]
316 |
317 | to_add = []
318 | to_add.append({'component_1':5,'component_3':(13,14,15),'component_2':((9,90),(10,100)),})
319 | to_add.append({'component_1':6,'component_3':(16,17,18),'component_2':((11,110),(12,120)),})
320 | to_add.append({'component_1':9,'component_3':(25,26,27),})
321 | trash = map(allocator.add,to_add)
322 | #allocator.add(to_add2)
323 | allocator._defrag()
324 | assert np.all(d1[:9] == np.array([1,2,3,4,5,6,7,8,9]))
325 |
326 | #to_add1 = {'component_1':2,'component_3':8,'component_2':7,}
327 | #to_add2 = {'component_1':5,'component_3':2,}
328 | #allocator.add(to_add1)
329 | #allocator.add(to_add2)
330 | #allocator._defrag()
331 | #to_add1 = {'component_1':3,'component_3':8,'component_2':7,}
332 | #to_add2 = {'component_1':6,'component_3':2,}
333 | #allocator.add(to_add1)
334 | #allocator.add(to_add2)
335 | #allocator._defrag()
336 |
337 |
--------------------------------------------------------------------------------
/numpy_ecs/table.py:
--------------------------------------------------------------------------------
1 | '''
2 | Classes to facilitate allocating guids in groups of class_ids
3 |
4 | Keeps sizes of allocated areas in a table
5 |
6 | class_id guid component_1 component_2 ...
7 | -----
8 | | 001 7 1
9 | | 003 6 1
10 | 11... 004 4 1
11 | | 005 6 1
12 | | 007 5 1
13 | -----
14 | | 008 4 0
15 | 10... 010 6 0
16 | | 011 2 0
17 | -----
18 |
19 | This file is part of Numpy-ECS.
20 | Copyright (C) 2016 Elliot Hallmark (permafacture@gmail.com)
21 |
22 | Numpy-ECS is free software: you can redistribute it and/or modify
23 | it under the terms of the GNU General Public License as published by
24 | the Free Software Foundation, either version 2 of the License, or
25 | (at your option) any later version.
26 |
27 | Data Oriented Python is distributed in the hope that it will be useful,
28 | but WITHOUT ANY WARRANTY; without even the implied warranty of
29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30 | GNU General Public License for more details.
31 |
32 | You should have received a copy of the GNU General Public License
33 | along with this program. If not, see .
34 | '''
35 |
36 | from collections import MutableMapping, Sequence
37 | from types import GeneratorType
38 |
39 | INDEX_SEPERATOR = '__to__' # 'ie: index from component1__to__component2
40 |
41 | #TODO make row a numpy array and delete TableRow
42 | class TableRow(Sequence):
43 | '''A tuple that can be added to another tuple of the same size'''
44 | def __init__(self,*values):
45 | if isinstance(values[0],GeneratorType):
46 | self.values = tuple(values[0])
47 | elif isinstance(values[0],tuple):
48 | self.values = values[0]
49 | else:
50 | self.values = tuple(values)
51 |
52 | def __add__(self,other):
53 | other = tuple(other)
54 | assert len(other) == len(self.values), "Must add item of same length"
55 | return TableRow(this+that for this,that in zip(self.values,other))
56 |
57 | def __sub__(self,other):
58 | other = tuple(other)
59 | assert len(other) == len(self.values), "Must add item of same length"
60 | return TableRow(this-that for this,that in zip(self.values,other))
61 |
62 | def __eq__(self,other):
63 | return self.values == other
64 |
65 | def __ne__(self,other):
66 | return self.values != other
67 |
68 | def __repr__(self):
69 | return "TableRow: %s" %(self.values,)
70 |
71 | def __len__(self):
72 | return len(self.values)
73 |
74 | def __iter__(self):
75 | return iter(self.values)
76 |
77 | def copy(self):
78 | return TableRow(*self.values)
79 |
80 | def __getitem__(self,index):
81 | return self.values[index]
82 |
83 | def slice_is_not_empty(s):
84 | #print " ",s.start,s.stop-s.start
85 | return s.start != s.stop
86 |
87 | class Table(object):
88 | '''An allocation table that has column names (component names) and major
89 | rows (class ids) and minor rows (guids). The minor rows are divisions of
90 | the major rows. Class ids are a tuple which are the binary of
91 | what columns are present in the guid row.
92 |
93 | keeps track of sizes and starts of allocated guids, and provides
94 | functionality for adding and deleting guids, as well as giving the slices
95 | at which the values of guids can be found in a Component arrays and for
96 | defragging those arrays.
97 | '''
98 | def __init__(self,column_names,class_ids):
99 | '''column names is a tuple of strings that define, in order, the names
100 | of the columns present in this table. class_ids is a tuple of tuples
101 | that determines the order of the major rows (essential for keeping
102 | related arrays contiguous with respect to each other)'''
103 | self.__col_names = tuple(column_names)
104 | self.__row_length = len(column_names)
105 | self.__row_format = ''.join((" | {:>%s}"%(len(name)) for name in column_names))
106 | self._staged_adds = dict()
107 | self.known_class_ids = tuple(class_ids)
108 | self.class_ids = list()
109 | self.guids = list()
110 | self.starts = list()
111 | self.sizes = list()
112 |
113 | @property
114 | def column_names(self):
115 | return self.__col_names
116 |
117 | #@property
118 | #def guid_columns(self):
119 | # return zip(*self.sizes)
120 |
121 | #@property
122 | #def class_id_columns(self):
123 | # return zip(*self.as_class_id_table())
124 |
125 | def entity_class_from_tuple(self,sizes_tuple):
126 | '''takes a component tuple like (5,5,0,2,2,2)
127 |
128 | returns a normalized tuple that is a class id, like (1,1,0,1,1,1)'''
129 | assert len(sizes_tuple) == self.__row_length, "too many values"
130 | # Used to convert to integer, but that's unnecessary
131 | #return sum(map(lambda (i,x): 0 if x ==0 else (x/x)*2**i,
132 | # enumerate(sizes_tuple[::-1])))
133 | return tuple(0 if x==0 else x/x for x in sizes_tuple)
134 |
135 | #def entity_class_from_dict(self,dictionary):
136 | # '''Creates a class id from a dictionary keyed by column_names'''
137 | # cols = self.column_names
138 | # keys = set(dictionary.keys())
139 | # #TODO make this one assert, not a loop
140 | # for name in keys:
141 | # assert(name in cols),"unknown class_id %s"%(name,)
142 | # return tuple(1 if name in keys else 0 for name in cols)
143 |
144 | # assert len(sizes_tuple) == self.__row_length, "too many values"
145 | # # Used to convert to integer, but that's unnecessary
146 | # #return sum(map(lambda (i,x): 0 if x ==0 else (x/x)*2**i,
147 | # # enumerate(sizes_tuple[::-1])))
148 | # return tuple(0 if x==0 else x/x for x in sizes_tuple)
149 |
150 | def stage_add(self,guid,value_tuple):
151 | assert guid not in self.guids, "guid must be unique"
152 | assert guid not in {t[0] for lst in self._staged_adds.values() \
153 | for t in lst}, "cannot restage a staged guid"
154 | ent_class = self.entity_class_from_tuple(value_tuple)
155 | assert ent_class in self.known_class_ids, \
156 | "added entity must corispond to a class id in the allocation schema"
157 | #print "guid %s is class %s"%(guid,ent_class)
158 | self._staged_adds.setdefault(ent_class,
159 | list()).append((guid,value_tuple))
160 |
161 | def stage_delete(self,guid):
162 | '''mark guid None so it can be removed later'''
163 | self.guids[self.guids.index(guid)] = None
164 |
165 | def make_starts_table(self):
166 | '''Create a list of tuples where each value is the start index of that
167 | element. (Table is the sizes)'''
168 | start = TableRow(*(0,)*self.__row_length)
169 | table = [start]
170 | for row in self.sizes:
171 | start = row + start
172 | table.append(start)
173 | return table
174 |
175 | #def class_sizes_table(self):
176 | # '''return a list of rows where each row is the size of the class_id's
177 | # in this table. This can be zipped with class_ids.'''
178 | # result = []
179 | # empty_row = lambda : TableRow(*(0,)*self.__row_length)
180 | # row = empty_row()
181 | # known_ids = iter(self.known_class_ids)
182 | # this_id = next(known_ids)
183 | # for class_id, size_row in zip(self.class_ids,self.sizes):
184 | # while class_id != this_id:
185 | # result.append(row)
186 | # row = empty_row()
187 | # this_id = next(known_ids)
188 | # row+= size_row
189 | # result.append(row)
190 | # #if class_ids is exausted but not all known ids were reached,
191 | # # add empty rows
192 | # for empty in known_ids:
193 | # result.append(empty_row())
194 | # return result
195 |
196 | def section_slices(self):
197 | #TODO doc string is wrong
198 | #TODO change to use class_size_table
199 | '''return a list of slices that represent the portions of each column
200 | that corrispond to the known class ids in order. For known ids not
201 | present in class_ids, a zero sized slice is used'''
202 | known_ids = self.known_class_ids
203 | id_column = self.class_ids
204 | expressed_ids = filter(lambda x: x in id_column,known_ids)
205 | starts = map(id_column.index,expressed_ids)
206 | assert all(starts[x]= free_start for start,
346 | free_start in zip(current_start,free_start))),\
347 | "next start must be larger than current free start"
348 | if guid is None:
349 | current_start += size_tuple
350 | deallocs += size_tuple
351 | else:
352 | #shift data back to cover deletes
353 | #print " counting %s of %s"%(guid,class_id)
354 | new_class_ids.append(class_id)
355 | new_guids.append(guid)
356 | new_sizes.append(size_tuple)
357 | if current_start != free_start:
358 | #print "size_tuple:\n",size_tuple
359 | sources.append(tuple(slice(start,start+size,1) for start,
360 | size in zip(current_start,size_tuple)))
361 | targets.append(tuple(slice(start,start+size,1) for start,
362 | size in zip(free_start,size_tuple)))
363 | #print " small alloc for guid",guid,"to",targets[-1]
364 | free_start += size_tuple
365 | current_start += size_tuple
366 | #deal with adds
367 | for added_guid, size_tuple in self._staged_adds.get(class_id,()):
368 | size = TableRow(size_tuple)
369 | allocs += size
370 | #print" adding guid %s as %s"%(added_guid,class_id)
371 | new_class_ids.append(class_id)
372 | new_guids.append(added_guid)
373 | new_sizes.append(size)
374 | #free_start = current_start + allocs #TODO part of last debugging
375 | free_start += allocs
376 | new_ends += allocs
377 | new_ends -= deallocs
378 | #after inserting adds, write the big change to sources and targets
379 | #Note: empty slices are added here, and are filtered out when we
380 | # columnize the row data
381 | if current_start != free_start:
382 | source = tuple(slice(start,end,1) for start,end in zip(
383 | current_start,ends))
384 | target = tuple(slice(start,end,1) for start,end in zip(
385 | free_start,new_ends))
386 | #print "adding:\n",source,target
387 | sources.append(source)
388 | targets.append(target)
389 | #print "big alloc for class",class_id
390 | #for s,t in zip(source,target): print s,"-->",t
391 | #print "starts:\n %s| %s\n %s| %s\n" % (current_start, ends, free_start,new_ends)
392 | #print "starts: %s | %s" % (current_start - free_start, ends - new_ends)
393 | current_start = free_start.copy() #TODO part of last debugging
394 | ends = new_ends.copy()
395 | total_alloc += allocs
396 | this_class_id = class_id
397 |
398 | self.class_ids = new_class_ids
399 | self.guids = new_guids
400 | self.sizes = new_sizes
401 | self.starts = self.make_starts_table()
402 | self._staged_adds = {}
403 |
404 | #columnize the row data
405 | #TODO make this a generator?
406 | ret = []
407 | for new_capacity, col_sources, col_targets in zip(
408 | new_ends, zip(*sources), zip(*targets)):
409 | #print "sources:"
410 | col_sources = tuple((s for s in col_sources if slice_is_not_empty(s)))
411 | #print "targets:"
412 | col_targets = tuple((t for t in col_targets if slice_is_not_empty(t)))
413 | #Assert that the above did what we expect
414 | if __debug__ == True:
415 | for s,t in zip(col_sources,col_targets):
416 | assert s.stop-s.start == t.stop-t.start, \
417 | 'source size and target size must match'
418 | assert len(col_sources) == len(col_targets)
419 | ret.append((new_capacity,col_sources,col_targets))
420 |
421 | return ret
422 |
423 | def slices_from_guid(self,guid):
424 | idx = self.guids.index(guid)
425 | starts = self.starts[idx]
426 | sizes = self.sizes[idx]
427 | return (slice(start,start+size,1) for start,size in zip(starts,sizes))
428 |
429 | #def as_class_id_table(self):
430 | # ids = self.class_ids
431 | # idx = 0
432 | # cur_id = ids[idx]
433 | # id = ids[idx]
434 | # result = list()
435 | # while idx < len(ids)
436 | # tmp_result = TableRow(*(0,)*self.__row_length)
437 | # while id == cur_id:
438 |
439 | #def __len__(self): return len(self.guids)
440 | #def __iter__(self): return iter(self.guids)
441 | #def __contains__(self,key): return key in self.guids
442 | #def keys(self): return tuple(self.guids)
443 | #def values(self): return tuple(self.rows)
444 | #def items(self): return zip(self.guids,self.rows)
445 |
446 | ##def add_key(self,key,default=None):
447 | ## assert key not in self.ids, "key %s already exists in table" % (key,)
448 | ## self.ids.append(key)
449 | ## if default is None:
450 | ## self.rows.append(TableRow(*(0,)*self.__row_length))
451 | ## else:
452 | ## assert len(value) == self.__row_length,\
453 | ## "%s does not have len %s"%(value,self.__row_length)
454 | ## self.rows.append(TableRow(*value))
455 |
456 | #def get_guid(self,key):
457 | # if key in self.guids:
458 | # return self.rows[self.guids.index(key)]
459 | # else:
460 | # return TableRow(*(0,)*self.__row_length)
461 | #
462 | #def __getitem__(self,key):
463 | # assert key in self.guids, "guid %s not in Table" % (key,)
464 | # return self.rows[self.guids.index(key)]
465 |
466 | #def __setitem__(self,key,value):
467 | # #TODO should this insert into proper class_id?
468 | # assert key in self.guids, "Key %s not in table Table" % (key,)
469 | # assert len(value) == self.__row_length,\
470 | # "%s does not have len %s"%(value,self.__row_length)
471 | # self.rows[self.guids.index(key)] = TableRow(*value)
472 |
473 | #def __delitem__(self,key):
474 | # raise AttributeError("Table does not support removal of keys")
475 |
476 | def show_sizes(self):
477 | formatter = self.__row_format.format
478 | ret_str = " guid"
479 | ret_str += formatter(*self.__col_names)
480 | ret_str += "\n"
481 | for guid,size_tuple in zip(self.guids, self.sizes):
482 | ret_str += "{:>5}".format(guid)
483 | ret_str += formatter(*size_tuple.values)
484 | ret_str += "\n"
485 | return ret_str
486 |
487 | def show_starts(self):
488 | formatter = self.__row_format.format
489 | ret_str = " guid"
490 | ret_str += formatter(*self.__col_names)
491 | ret_str += "\n"
492 | for guid,size_tuple in zip(self.guids, self.starts):
493 | ret_str += "{:>5}".format(guid)
494 | ret_str += formatter(*size_tuple.values)
495 | ret_str += "\n"
496 | return ret_str
497 |
498 | def __str__(self):
499 | return ""
500 |
501 | #def column_names_from_mask(self,mask):
502 | # '''takes a mask as a tuple or integer (where the binary representation
503 | # of the integer is the mask) and returns only those names.
504 |
505 | # (0,0,1,1) and 3 are identical masks'''
506 |
507 | # names = self.__col_names
508 | # if isinstance(mask,int):
509 | # mask = bin(mask)[2:]
510 | # return tuple(name for name,exists in zip(names[::-1],mask[::-1]))
511 | # elif isinstance(mask,tuple):
512 | # raise NotImplementedError("easy to implement if needed")
513 |
514 | if __name__ == '__main__':
515 | t = Table(('one','two'),((1,0),(1,1),(0,1)))
516 | try:
517 | t.stage_add(1,(3,3,3))
518 | except AssertionError:
519 | pass
520 | else:
521 | raise AssertionError("added three columns to two column table")
522 |
523 | #test adds and compression work
524 | t.stage_add(1,(0,3)) #added third
525 | t.stage_add(2,(3,0)) #this will be added first, because it is class_id == (1,0)
526 | t.stage_add(3,(3,3)) #added second
527 |
528 | if True: #for indentation
529 | #staging a guid already staged should over-write it
530 | try:
531 | t.stage_add(1,(10,10))
532 | except AssertionError:
533 | pass
534 | else:
535 | print "Test failed: re-added staged guid"
536 |
537 | for capacity, sources, targets in t.compress():
538 | assert capacity == 6, "capacity set to %s instead of 6" % alloc
539 | assert not sources, "no data should be moved:" + str(zip(sources,targets))
540 |
541 |
542 | #check that data ends up where it should
543 | expected = [(slice(0,3,1),slice(0,0,1)),
544 | (slice(3,6,1),slice(0,3,1)),
545 | (slice(6,6,1),slice(3,6,1)),]
546 | #print "starts:\n",t.print_starts()
547 | #print "sizes:\n",t.print_sizes()
548 |
549 | for guid,e in zip((2,3,1),expected):
550 | this_slice = tuple(t.slices_from_guid(guid))
551 | assert this_slice == e, "got wrong slices for guid: %s"%guid
552 |
553 | #test that adding an already allocated guid is illegal
554 | try:
555 | t.stage_add(1,(10,10))
556 | except AssertionError:
557 | pass
558 | else:
559 | print "Test failed: re-added allocated guid"
560 |
561 | #test adding an entity to a non-empty table
562 | t.stage_add(4,(10,10))
563 | print "staged_adds:",t._staged_adds
564 |
565 | for n, (capacity, sources, targets) in enumerate(t.compress()):
566 | assert capacity == 16, "capacity set to %s instead of 16" % alloc
567 | if n == 0:
568 | assert not sources and not targets, "first component does not move"
569 | elif n == 1:
570 | assert sources == (slice(3,6,1),), "guid 1 started at slice(3,6,1)"
571 | assert targets == (slice(13,16,1),), "guid 1 moves to slice(13,16,1)"
572 |
573 | #test getting slices for sections
574 | #TODO need to test that non-continuous columns fail and other edge cases
575 | t = Table(('one','two'),((1,0),(1,1),(0,1)))
576 | t.stage_add(1 ,(0,3))
577 | t.stage_add(2 ,(0,3))
578 | t.stage_add(3 ,(0,3))
579 | t.stage_add(4 ,(0,3))
580 | t.stage_add(5 ,(0,3))
581 | t.stage_add(6 ,(3,0))
582 | t.stage_add(7 ,(3,0))
583 | t.stage_add(8 ,(3,0))
584 | t.stage_add(9 ,(3,0))
585 | t.stage_add(10,(3,0))
586 | t.stage_add(13,(3,3))
587 | t.stage_add(14,(3,3))
588 | t.stage_add(15,(3,3))
589 | t.stage_add(16,(3,3))
590 | t.stage_add(17,(3,3))
591 | t.stage_add(18,(3,3))
592 | t.compress()
593 | expected = {(1,0):slice(0,5,1),(1,1):slice(5,11,1),(0,1):slice(11,None,1)}
594 | assert t.section_slices()==expected, "section_slices should return expected result"
595 | assert t.mask_slices(('one','two'),())[0] == {'one':slice(15, 33, None), 'two':slice(0, 18, None)}
596 |
--------------------------------------------------------------------------------
/planning.txt:
--------------------------------------------------------------------------------
1 | Renderable():
2 | inputs:
3 | vertices
4 | colors
5 | outputs:
6 | IO
7 |
8 | Transformable():
9 | inputs:
10 | position
11 | angle
12 | outputs:
13 | vertices
14 |
15 | Colored():
16 | inputs:
17 | color
18 | outputs:
19 | colors
20 |
21 | (Transformable, Colored) -> Renderable
22 |
23 | Car(Transformable, Colored):
24 | inputs:
25 | models
26 | outputs:
27 | vertices
28 | colors
29 |
30 | ^ That's too complicated. Let's make it more atomic.
31 |
32 | Renderable(vertices, colors):
33 | outputs:
34 | IO
35 |
36 | SolidColored_Rotateable_RegularPolygon(n,r,color,pos,angle):
37 | n,r and color are immutable
38 | pos and angle are mutable
39 | xhelper and yhelper are internal state
40 |
41 | outputs:
42 | vertices
43 | colors
44 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup(
4 | name = "Numpy-ECS",
5 | version = "0.01",
6 | author = "Elliot Hallmark",
7 | author_email = "permafacture@gmail.com",
8 | description = ("An Entity Component System framework built on numpy arrays"),
9 | license = "GPLv2",
10 | keywords = "data oriented programming, ECS",
11 | url = "https://github.com/Permafacture/data-oriented-pyglet",
12 | install_requires = ['numpy','pyglet'],
13 | packages=['numpy_ecs',],
14 | )
15 |
--------------------------------------------------------------------------------