.. Copyright (c) 2018-2024 William Emerison Six Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Lambda Stack - Demo 18 ====================== Purpose ^^^^^^^ Remove repetition in the coordinate transformations, as previous demos had very similar transformations, especially from camera space to NDC space. Each edge of the graph of objects should only be specified once per frame. .. figure:: static/demo18-1.png :align: center :alt: Demo 12 :figclass: align-center Full Cayley graph. Noticing in the previous demos that the lower parts of the transformations have a common pattern, we can create a stack of functions for later application. Before drawing geometry, we add any functions to the top of the stack, apply all of our functions in the stack to our modelspace data to get NDC data, and before we return to the parent node, we pop the functions we added off of the stack, to ensure that we return the stack to the state that the parent node gave us. To explain in more detail --- What's the difference between drawing paddle 1 and the square? Here is paddle 1 code .. literalinclude:: ../src/demo17/demo.py :language: python :start-after: doc-region-begin 2ced82a1c3de464adbfe5d303faffdd2314c17c2 :end-before: doc-region-end 2ced82a1c3de464adbfe5d303faffdd2314c17c2 Here is the square's code: .. literalinclude:: ../src/demo17/demo.py :language: python :start-after: doc-region-begin 23cd906b0bec259766279f1a9277922719cf1e2b :end-before: doc-region-end 23cd906b0bec259766279f1a9277922719cf1e2b The only difference is the square's model-space to paddle1 space. Everything else is exactly the same. In a graphics program, because the scene is a hierarchy of relative objects, it's unwise to put this much repetition in the transformation sequence. Especially if we might change how the camera operates, or from perspective to ortho. It would required a lot of code changes. And I don't like reading from the bottom of the code up. Code doesn't execute that way. I want to read from top to bottom. When reading the transformation sequences in the previous demos from top down the transformation at the top is applied first, the transformation at the bottom is applied last, with the intermediate results method-chained together. (look up above for a reminder) With a function stack, the function at the top of the stack (f5) is applied first, the result of this is then given as input to f4 (second on the stack), all the way down to f1, which was the first fn to be placed on the stack, and as such, the last to be applied. (Last In First Applied - LIFA) :: |-------------------| (MODELSPACE) | | (x,y,z)-> | f5 |-- |-------------------| | | ------------------------- | | |-------------------| | | | ->| f4 |-- |-------------------| | | ------------------------- | | |-------------------| | | | ->| f3 |-- |-------------------| | | ------------------------- | | |-------------------| | | | ->| f2 |-- |-------------------| | | ------------------------- | | |-------------------| | | | ->| f1 |--> (x,y,z) NDC |-------------------| So, in order to ensure that the functions in a stack will execute in the same order as all of the previous demos, they need to be pushed onto the stack in reverse order. This means that from modelspace to world space, we can now read the transformations FROM TOP TO BOTTOM!!!! SUCCESS! Then, to draw the square relative to paddle one, those six transformations will already be on the stack, therefore only push the differences, and then apply the stack to the paddle's modelspace data. How to Execute ^^^^^^^^^^^^^^ On Linux or on MacOS, in a shell, type "python src/demo18/demo.py". On Windows, in a command prompt, type "python src\\demo18\\demo.py". Move the Paddles using the Keyboard ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ============== ============================================== Keyboard Input Action ============== ============================================== *w* Move Left Paddle Up *s* Move Left Paddle Down *k* Move Right Paddle Down *i* Move Right Paddle Up *d* Increase Left Paddle's Rotation *a* Decrease Left Paddle's Rotation *l* Increase Right Paddle's Rotation *j* Decrease Right Paddle's Rotation *UP* Move the camera up, moving the objects down *DOWN* Move the camera down, moving the objects up *LEFT* Move the camera left, moving the objects right *RIGHT* Move the camera right, moving the objects left *q* Rotate the square around it's center *e* Rotate the square around paddle 1's center ============== ============================================== Description ^^^^^^^^^^^ Function stack. Internally it has a list, where index 0 is the bottom of the stack. In python we can store any object as a variable, and we will be storing functions which transform a vertex to another vertex, through the "modelspace_to_ndc" method. .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin 17f3fd5d2ee2d55faeeff6e71eeb4dbe288c7842 :end-before: doc-region-end 17f3fd5d2ee2d55faeeff6e71eeb4dbe288c7842 There is an example at the bottom of src/demo18/demo.py .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin 1b3386db3fb40d61e80828a90a820b3a235ca940 :end-before: doc-region-end 1b3386db3fb40d61e80828a90a820b3a235ca940 Define four functions, which we will compose on the stack. Push identity onto the stack, which will will never pop off of the stack. .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin 6e7a7ee4a8493ddc6478bcaf2dbb6fb4a2a9753a :end-before: doc-region-end 6e7a7ee4a8493ddc6478bcaf2dbb6fb4a2a9753a .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin ccac3b1b9bc8c759e45929c467274c763f7671c7 :end-before: doc-region-end ccac3b1b9bc8c759e45929c467274c763f7671c7 .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin 24f3367f7ffaefa3d882a20f772ce7f089049391 :end-before: doc-region-end 24f3367f7ffaefa3d882a20f772ce7f089049391 .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin b2d4aee29938ffba8e023e2d523e5a8b7b400bee :end-before: doc-region-end b2d4aee29938ffba8e023e2d523e5a8b7b400bee .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin 95d6bcea04206261224089df1734055b9b4196d3 :end-before: doc-region-end 95d6bcea04206261224089df1734055b9b4196d3 .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin 55dd21f698dbbde5bd5d1b332279d099a315f771 :end-before: doc-region-end 55dd21f698dbbde5bd5d1b332279d099a315f771 .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin e09b968f9c8a2f9db13475d1c9ac310d94e54a2a :end-before: doc-region-end e09b968f9c8a2f9db13475d1c9ac310d94e54a2a Event Loop ^^^^^^^^^^ .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin 6d86d07154c99ed6e1c3feab73545d184153f9ae :end-before: doc-region-end 6d86d07154c99ed6e1c3feab73545d184153f9ae :: ... In previous demos, camera_space_to_ndc_space_fn was always the last function called in the method chained pipeline. Put it on the bottom of the stack, by pushing it first, so that "modelspace_to_ndc" calls this function last. Each subsequent push will add a new function to the top of the stack. .. math:: \vec{f}_{c}^{ndc} .. figure:: static/demo18-2.png :align: center :alt: Demo 12 :figclass: align-center .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin 48bd13153ce54db3f6b9ea5833e91820b7d8b020 :end-before: doc-region-end 48bd13153ce54db3f6b9ea5833e91820b7d8b020 Unlike in previous demos in which we read the transformations from model space to world space backwards; this time because the transformations are on a stack, the fns on the model stack can be read forwards, where each operation translates/rotates/scales the current space The camera's position and orientation are defined relative to world space like so, read top to bottom: .. math:: \vec{f}_{c}^{w} .. figure:: static/demo18-7.png :align: center :alt: Demo 12 :figclass: align-center .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin f217e71c7f5a228622d5db86e6fe0dec1e072dca :end-before: doc-region-end f217e71c7f5a228622d5db86e6fe0dec1e072dca But, since we need to transform world-space to camera space, they must be inverted by reversing the order, and negating the arguments Therefore the transformations to put the world space into camera space are. .. math:: \vec{f}_{w}^{c} .. figure:: static/demo18-3.png :align: center :alt: Demo 12 :figclass: align-center .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin c0dcf40149c0b85d84f13b4421a114409a274432 :end-before: doc-region-end c0dcf40149c0b85d84f13b4421a114409a274432 draw paddle 1 ~~~~~~~~~~~~~ Unlike in previous demos in which we read the transformations from model space to world space backwards; because the transformations are on a stack, the fns on the model stack can be read forwards, where each operation translates/rotates/scales the current space .. math:: \vec{f}_{p1}^{w} .. figure:: static/demo18-4.png :align: center :alt: Demo 12 :figclass: align-center .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin 7de7248650b2809520898faed65be4050d2b441a :end-before: doc-region-end 7de7248650b2809520898faed65be4050d2b441a for each of the modelspace coordinates, apply all of the procedures on the stack from top to bottom this results in coordinate data in NDC space, which we can pass to glVertex3f .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin 6b57a4425b47582cdfb194a1c2fbb3ac9a17a163 :end-before: doc-region-end 6b57a4425b47582cdfb194a1c2fbb3ac9a17a163 draw the square ~~~~~~~~~~~~~~~ since the modelstack is already in paddle1's space, and since the blue square is defined relative to paddle1, just add the transformations relative to it before the blue square is drawn. Draw the square, and then remove these 4 transformations from the stack (done below) .. math:: \vec{f}_{s}^{p1} .. figure:: static/demo18-5.png :align: center :alt: Demo 12 :figclass: align-center .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin 87d309a76468a5dd49f5805f739932d7a1b4dac1 :end-before: doc-region-end 87d309a76468a5dd49f5805f739932d7a1b4dac1 Now we need to remove fns from the stack so that the lambda stack will convert from world space to NDC. This will allow us to just add the transformaions from world space to paddle2 space on the stack. .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin 6e83cdfe078bb103bf04c3d53a4c4ec7cb22ef60 :end-before: doc-region-end 6e83cdfe078bb103bf04c3d53a4c4ec7cb22ef60 since paddle2's model_space is independent of paddle 1's space, only leave the view and projection fns (1) - (4) draw paddle 2 ~~~~~~~~~~~~~ .. math:: \vec{f}_{p2}^{w} .. figure:: static/demo18-6.png :align: center :alt: Demo 12 :figclass: align-center .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin 9206a08662c91ad536b41641910f7e8e951f7c9e :end-before: doc-region-end 9206a08662c91ad536b41641910f7e8e951f7c9e remove all fns from the function stack, as the next frame will set them clear makes the list empty, as the list (stack) will be repopulated the next iteration of the event loop. .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin 4d0f02db53413ede074a8693bb19e68292db3bd4 :end-before: doc-region-end 4d0f02db53413ede074a8693bb19e68292db3bd4 Swap buffers and execute another iteration of the event loop .. literalinclude:: ../src/demo18/demo.py :language: python :start-after: doc-region-begin 64809e2fccc9daa3f97239991d905b7fc3f03d62 :end-before: doc-region-end 64809e2fccc9daa3f97239991d905b7fc3f03d62 Notice in the above code, adding functions to the stack is creating a shared context for transformations, and before we call "glVertex3f", we always call "modelspace_to_ndc" on the modelspace vertex. In Demo 19, we will be using OpenGL 2.1 matrix stacks. Although we don't have the code for the OpenGL driver, given that you'll see that we pass modelspace data directly to "glVertex3f", it should be clear that the OpenGL implementation must fetch the modelspace to NDC transformations from the ModelView and Projection matrix stacks.