Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Quaternions

in Questions Posts: 2,044

@dave1707 - @Simeon - in the dim and distant past a Codea explorer (@ignatz) wrote a demo with well documented code for the use of quaternions. I can’t run it as an error is thrown up regarding a reserved word setfenv which is recognised by the system but I can’t find documented anywhere. In addition I think it has been shelved or modified as it is the source of the error. @loopspace introduced the feature to @ignatz. So could someone point me to details on this functionality or - a way round it.

Apparently setfenv enabled the ability to run several demos, essentially separate projects, in a single multiple tab project. I think similar code is used in some of the example projects for Codea/Craft.

Comments

  • dave1707dave1707 Mod
    Posts: 8,976

    @Bri_G What demo are you trying to run. Have you tried a forum search on setfenv.

  • JohnJohn Admin Mod
    Posts: 643

    @Bri_G setfenv is no longer available in Lua 5.2 as far as I know. You can recreate it, if you search it on google there are a bunch of recommended solutions: https://leafo.net/guides/setfenv-in-lua52-and-above.html

  • Posts: 2,044

    @dave1707 - I'm still trying to find the demo on @ignatz 's website for which I have the code - there are several on his website but so far can't match against the code I have. Unfortunately there are problems with several of his demos - still digging.

    @John - thanks for the prompt reply and link - very interesting link. Am digging further as just adding the two functions, in your reference, to the existing code still throws up errors. I'm assuming placement of the code and possible format are important. If I manage to progress this I'll post the working code for future reference. Thanks again.

  • As there is now the quat built-in, I'm not sure that the old quaternion code is all that useful any more.

    I have my explanation of quaternions at https://loopspace.mathforge.org/HowDidIDoThat/Codea/Quaternions/, and my library code is part of my vecExt library which is available at https://github.com/loopspace/Codea-Library-Maths/blob/master/VecExt.lua.

    What are you trying to achieve? I may be able to help.

  • edited November 2020 Posts: 2,044
    @LoopSpace - thanks for the links. When you say that quat is built in do you mean quaternions are now covered within Codea? I have a couple of problems which are related to rotating models, one is smooth 360rotation the other is model distortion on rotation.
    I thought the Craft system for rotation suffered from this so was trying to extract the quaternion code from @Ignatz 's flying tutorial - but, so far, haven't found the detail from his website. II'll read through your explanation and code - thanks again.
  • Posts: 2,044

    @LoopSpace - just a quick, very impressed with the library - certainly needs a mathematician to appreciate it. Sadly - I’m not.But installed the vecExt library and attempted to run your example program for Quaternions but failed. I pasted the library code into a tab. Do you need the full library present to use the vecExt library?

  • @Bri_G You might need a little history lesson here ...

    I originally wrote a library called Quaternion for implementing quaternions. The underlying object here was a table. Then Codea included a vec4 userdata and also I learnt about metatables and how to extend userdata. This led to me to re-implement my quaternion code using vec4s as the underlying object. Finally, Codea introduced the quat userdata to be a genuine quaternion object. It doesn't have the full functionality that I would want so I redesigned my code to add that to it.

    The post on Quaternions hosted at mathforge was written in the first phase of this. So the "example program" at the end is horribly out of date. Don't try it. I should update it, but honestly my todo list is so long that it is visible from the moon ... Rather, use that post to understand what quaternions can give you.

    The VecExt code on github is my latest code (well, I might have a few bug fixes on my iPad, but broadly speaking it is). It should work as a tab in a project (I use toadkick's cmodule for side-ways importing of code so that I can load individual tabs from projects programatically rather than via dependencies but I tried to design it so that it will also work without that). I do have plenty of examples of using the modern code, I'll dig one out for you later.

    I remember well discussing quaternions with Ignatz and the flying demo was the one that we used to go back and forth. It therefore suffers a bit since it was updated a bit ad hoc, with bits of it being rewritten at different times as the discussion progressed. So although it has much in it that demonstrates quaternions, I wouldn't recommend it as a template to follow.

  • Posts: 2,044
    @LoopSpace - thanks again for the feedback and the history lesson - one clear message nothing ever stays still !! I should have learnt that by now with Codea. Answered another question - your code used an import function from toadkick. Is there any documentation on Codea quat?

    I'm begining to believe that most of my problem stems from using obj models with vector colours included. Seems to play havoc with proportion during rotation and hidden feature removal. Textured models work fine, so I may texture my simple models and park quaternions until I get some real free time.

    Thanks again and good luck with your to-do climb.
  • @Bri_G quats are in the codea reference documentation under Vectors.

  • Posts: 2,044

    @piinthesky - thanks for that, digesting them now. Now allI need to do is work out how to use it. Thanks again.

  • edited November 2020 Posts: 509

    I've uploaded a demo of my code, and other aspects, to github:

    https://github.com/loopspace/Codea-Quaternion-Demo

    This uses my extensions to the quaternion code with Craft models with textures, so it covers what you (@Bri_G) have mentioned, though it may not cover it in the way you want.

  • Posts: 2,044

    @LoopSpace - wow, that is some demo. Not only does it provide updated working code but the explanations are excellent. It also provides an excellent example of PseudoMesh, which I have struggled with in the past. But better yet - the grandkids love it.

    Seriously, thanks for following up on this - at the moment I am getting very little personal time to play and this code should help me appreciate quaternions and apply it to my own code. Thanks again.

  • edited November 2020 Posts: 509

    Not a problem! It's a program I wrote a while back so all I had to do was add some comments and check that it worked in standalone (ie without the importing stuff).

    Feel free to ask questions as and when they occur.

  • dave1707dave1707 Mod
    Posts: 8,976

    While we’re on the subject of Quaternions, I thought I’d dig up an old program to see if anyone can fix it. If you run the code and slide the x slider, the dice will rotate around the x axis, a line thru the sides 1 and 6. Restart and slide the y slider and the dice will rotate around the y axis, a line thru the sides 3 and 4. Restart and slide the z slider and it rotates around the z axis, sides 2 and 5. The problem is if the dice is rotated to some random x,y,z angles and then you try to rotate around each axis, sometimes it rotates around an axis correct, other times it doesn’t. Is there a quaternion calculation so that no matter how the dice is rotated, it will rotate around each axis correctly. I tried looking at the @LoopSpace example, but there’s too much code and quaternions don’t make any sense anyways.

    viewer.mode=STANDARD
    
    function setup()
        print("x axis = sides 1,6")
        print("y axis = sides 3,4")
        print("z axis = sides 2,5")
        parameter.integer("xx",-1,1,0)
        parameter.integer("yy",-1,1,0)
        parameter.integer("zz",-1,1,0)
        rectMode(CORNER)    
        scene = craft.scene()    
        assert(OrbitViewer, "Please include Cameras (not Camera) as a dependency")  
        viewer = scene.camera:add(OrbitViewer, vec3(0), 10, 0, 2000)
        rx,ry,rz=0,0,0
        createImg()
        createDice()
        pos=1
    end
    
    function update(dt)
        scene:update(dt)
    end
    
    function draw()
        background(0)
        update(DeltaTime)
        scene:draw() 
        dice.rotation=quat.eulerAngles(rx,ry,rz)
        rx=rx+xx
        ry=ry+yy
        rz=rz+zz
        fill(255)
        text(string.format("x =%3d    y =%3d    z =%3d",rx%360,ry%360,rz%360),WIDTH/2,HEIGHT-50)
    end
    
    function createImg()
        noSmooth()
        img=image(600,100)
        setContext(img)
        background(0, 0, 0, 255)
        fill(255,255,255)
        rect(0,0,600,100)
        fill(0,0,0)
        ellipse(50,50,20)
        ellipse(125,75,20) ellipse(175,25,20)
        ellipse(525,75,20) ellipse(550,50,20) ellipse(575,25,20)
        ellipse(425,75,20) ellipse(425,25,20) ellipse(475,75,20) ellipse(475,25,20)
        ellipse(350,50,20) ellipse(325,75,20) ellipse(325,25,20)
        ellipse(375,75,20) ellipse(375,25,20)
        ellipse(225,75,20) ellipse(250,75,20) ellipse(275,75,20)
        ellipse(225,25,20) ellipse(250,25,20) ellipse(275,25,20)
        setContext()
    end
    
    function createDice(px,py,pz)
        dice=scene:entity()
        dice.position=vec3(px,py,pz)
        dice.model = craft.model.cube(vec3(1,1,1))
        dice.material = craft.material(asset.builtin.Materials.Standard)
        dice.material.map = img
        local temp=dice.model.indices
        for z=#dice.model.indices,1,-1 do
            table.insert(temp,dice.model.indices[z])
        end
        dice.model.indices=temp
        local uvs1={}
        c=0
        for x=1,6 do
            table.insert(uvs1,vec2(c/6,0))
            table.insert(uvs1,vec2((c+1)/6,0))
            table.insert(uvs1,vec2((c+1)/6,1))
            table.insert(uvs1,vec2(c/6,1))
            c=c+1
        end
        dice.model.uvs=uvs1
    end
    
  • edited November 2020 Posts: 509

    @dave1707 I haven't run your code yet, but I suspect you are running in to the issue that rotations don't commute. That is to say, if you rotate an angle about the x-axis, then the y-axis, then reverse the rotation about the x-axis, then reverse the rotation about the y-axis you won't be back at the starting point.

    When you rotate about one axis, the other axes get rotated. So then further rotating about those axes will look different to how you expect.

    So when you rotate about x,y,z you need to specify the order of axes to rotate about. I don't remember what order Codea uses -- I will experiment to figure it out later when I'm on my iPad. In effect, whichever comes first can be thought of as being from the perspective of the die, and whichever comes last from the perspective of the user. The middle one will look wrong to both.

    There are ways to make it look more "natural", but they involve keeping track of the changes rather than going straight to the Euler angles. I'll have a play with your code to see if I can make minimal changes to illustrate.

    (Note: this isn't an issue specific to quaternions, it's behaviour of rotations in general and quaternions have it because any way of encoding rotations would have it.)

  • dave1707dave1707 Mod
    Posts: 8,976

    @LoopSpace I can rotate the dice in any direction on any axis numerous times and I can get back to the original starting position rotating any axis in any order.

  • edited December 2020 Posts: 918

    in the program below, i capture xx,yy,zz into the quat. i multiply dice.rotation by the quat.

    in rotation land a quat is a rotation. to add two rotations, you multiply by the quat you want to add. so the die rotates. newrot = oldrot*addedrot

    uncomment one or the other quat multiply line to switch from local to global rotations.

    if you put the new quaternion on the right (old*new) the rotation is local. if you put it the other way, the rotation is global.

    by local, i mean the due will always rotate around the same die-relative axis, like the 1-6 axis or the 2-5. by global, i mean it'll always be screen relative, y always screen vertical, and so on.

    with your scheme, if it got to a place where it wouldn't turn, that's called gimbal lock, and it's a thing that happens when you work with x y z angles instead of rotations.

    viewer.mode=STANDARD
    
    function setup()
        print("x axis = sides 1,6")
        print("y axis = sides 3,4")
        print("z axis = sides 2,5")
        parameter.integer("xx",-1,1,0)
        parameter.integer("yy",-1,1,0)
        parameter.integer("zz",-1,1,0)
        rectMode(CORNER)
        scene = craft.scene()
        assert(OrbitViewer, "Please include Cameras (not Camera) as a dependency")
        viewer = scene.camera:add(OrbitViewer, vec3(0), 10, 0, 2000)
        --rx,ry,rz=0,0,0
        createImg()
        createDice()
        pos=1
    end
    
    function update(dt)
        scene:update(dt)
    end
    
    function draw()
        background(0)
        update(DeltaTime)
        scene:draw()
        dice.rotation=dice.rotation*quat.eulerAngles(xx,yy,zz) -- local
        --dice.rotation = quat.eulerAngles(xx,yy,zz)*dice.rotation -- global
        --rx=rx+xx
        --ry=ry+yy
        --rz=rz+zz
        fill(255)
        --text(string.format("x =%3d    y =%3d    z =%3d",rx%360,ry%360,rz%360),WIDTH/2,HEIGHT-50)
    end
    
    function createImg()
        noSmooth()
        img=image(600,100)
        setContext(img)
        background(0, 0, 0, 255)
        fill(255,255,255)
        rect(0,0,600,100)
        fill(0,0,0)
        ellipse(50,50,20)
        ellipse(125,75,20) ellipse(175,25,20)
        ellipse(525,75,20) ellipse(550,50,20) ellipse(575,25,20)
        ellipse(425,75,20) ellipse(425,25,20) ellipse(475,75,20) ellipse(475,25,20)
        ellipse(350,50,20) ellipse(325,75,20) ellipse(325,25,20)
        ellipse(375,75,20) ellipse(375,25,20)
        ellipse(225,75,20) ellipse(250,75,20) ellipse(275,75,20)
        ellipse(225,25,20) ellipse(250,25,20) ellipse(275,25,20)
        setContext()
    end
    
    function createDice(px,py,pz)
        dice=scene:entity()
        dice.position=vec3(px,py,pz)
        dice.model = craft.model.cube(vec3(1,1,1))
        dice.material = craft.material(asset.builtin.Materials.Standard)
        dice.material.map = img
        local temp=dice.model.indices
        for z=#dice.model.indices,1,-1 do
            table.insert(temp,dice.model.indices[z])
        end
        dice.model.indices=temp
        local uvs1={}
        c=0
        for x=1,6 do
            table.insert(uvs1,vec2(c/6,0))
            table.insert(uvs1,vec2((c+1)/6,0))
            table.insert(uvs1,vec2((c+1)/6,1))
            table.insert(uvs1,vec2(c/6,1))
            c=c+1
        end
        dice.model.uvs=uvs1
    end
    
  • dave1707dave1707 Mod
    Posts: 8,976

    @RonJeffries Thanks for the change. That’s what I was looking for, a rotation around each axis no matter how the dice was turned. That gives a more natural tumble. I’ll have to look closer at your change and see exactly what’s happening.

  • dave1707dave1707 Mod
    Posts: 8,976

    @RonJeffries I looked at the change you made

    dice.rotation=dice.rotation*quat.eulerAngles(xx,yy,zz)
    

    and it doesn’t make any sense to me why it works.

  • dave1707dave1707 Mod
    Posts: 8,976

    Here’s my multiple dice program with the quat change suggested by @RonJeffries.

    @Simeon If I let this program run, it eventually crashes Codea because of memory. 2 things I noticed. It pauses for different lengths of time at about 15 seconds intervals. Sometimes it’s barely noticeable, other times it’s about .5 seconds. Also I’m displaying the memory usage at the top middle of the screen. That constantly increases until it crashes. If I uncomment the collectgarbage() in the draw function, memory usage remains low and I don’t notice the pauses. It’s like the auto collectgarbage can’t keep up.

    viewer.mode=FULLSCREEN
    
    -- display a bunch of dice
    
    function setup()
        sp=.01
        rectMode(CORNER)    
        scene = craft.scene()    
        assert(OrbitViewer, "Please include Cameras (not Camera) as a dependency")  
        viewer = scene.camera:add(OrbitViewer, vec3(0), 25, 0, 2000)
        createImg()
        tab={}
        for x=-10,10 do
            for y=-10,10 do
                createImg()
                table.insert(tab,createDice(x,y,math.random(-10,10)))
            end
        end  
    end
    
    function createImg()
        noSmooth()
        img=image(600,100)    
        setContext(img)
        background(0, 0, 0, 255)    
        fill(math.random(255),math.random(255),math.random(255)) 
        rect(0,0,600,100)
        noStroke()
        fill(math.random(255),math.random(255),math.random(255)) 
        ellipse(50,50,20)    
        ellipse(125,75,20) ellipse(175,25,20)    
        ellipse(525,75,20) ellipse(550,50,20) ellipse(575,25,20)
        ellipse(425,75,20) ellipse(425,25,20) ellipse(475,75,20) ellipse(475,25,20)    
        ellipse(350,50,20) ellipse(325,75,20) ellipse(325,25,20)
        ellipse(375,75,20) ellipse(375,25,20)    
        ellipse(225,75,20) ellipse(250,75,20) ellipse(275,75,20)    
        ellipse(225,25,20) ellipse(250,25,20) ellipse(275,25,20)
        stroke(200)
        strokeWidth(2)
        noFill()
        for z=0,5 do
            rect(z*100,0,101,101)       
        end
        setContext()    
    end
    
    function update(dt)
        scene:update(dt)
    end
    
    function draw()
        update(DeltaTime)
        scene:draw() 
        for a,b in pairs(tab) do        
            b.m.rotation=b.m.rotation*quat.eulerAngles(b.rx,b.ry,b.rz)
        end
        fill(255)
        text(collectgarbage("count")//1,WIDTH/2,HEIGHT-25)
        --collectgarbage()
    end
    
    function createDice(px,py,pz)
        local rx,ry,rz=math.random(-100,100)*sp,math.random(-100,100)*sp,math.random(-100,100)*sp
        local vx,vy,vz=math.random(-10,10),math.random(-10,10),math.random(-10,10)
        local m=scene:entity()
        m.position=vec3(px,py,pz)
        m.rotation=quat.eulerAngles(rx,ry,rz)
        m.model = craft.model.cube(vec3(1,1,1))
        m.material = craft.material(asset.builtin.Materials.Basic)
        m.material.map = img      
        temp=m.model.indices
        for z=#m.model.indices,1,-1 do
            table.insert(temp,m.model.indices[z])
        end
        m.model.indices=temp      
        local uvs1={}
        c=0
        for x=1,6 do
            table.insert(uvs1,vec2(c/6,0))
            table.insert(uvs1,vec2((c+1)/6,0))
            table.insert(uvs1,vec2((c+1)/6,1))
            table.insert(uvs1,vec2(c/6,1))
            c=c+1
        end
        m.model.uvs=uvs1  
        return({m=m,rx=rx,ry=ry,rz=rz,vx=vx,vy=vy,vz=vz})  
    end
    
  • Posts: 2,044
    Hi guys,

    Correct me if I'm wrong but I thought the use of euler angles was OK for supporting quaternions but not true quaternions. The code above is excellent and addresses most of my holes in knowledge but is it true quaternion maths ?
  • edited December 2020 Posts: 918

    it is almost certain that model.rotation is a quaternion or equivalent. a quaternion stores angles around the axes. the net effect is always an axis, typically not aligned with local or global axes, and an angle of rotation around that axis.

    let's pretend that a rotation/quat is just "anAngle". given some otherAngle, we want to "add" our new angle to the old one, like

    anAngle = anAngle + otherAngle.

    we can;t really do that, because we have, not just an angle, but a 3D rotation, made up of a bunch of angles, like 15x + 22y + 18z. the quat we get from eulerangles embeds all that info.

    quaternions are kind of matrices. given quats for our angle things that we wanted to add, with quats, you multiply. so, in quat terms we say

    anAngleQuat = anAngleQuat * otherAngleQuat.

    that basically means, whatever angle anAngleQuat is, add whatever angle otherAngleQuat is.

    plus the stuff about which side you multiply on. quat multiply is not commutative.

    increment*base is a global rotation change, that is, aligned with the world axes, while base*increment is local, aligned with the object's current local axes.

    it's weird. basically you just hold your nose and do it.

    p.s. i might have global vs local backward, i can never remember.

  • can't confirm the crash yet but will let it run. to avoid the garbage problem, i think you could cache the quat from euler at create time: it's constant.

  • dave1707dave1707 Mod
    Posts: 8,976

    @RonJeffries I tried reversing

    b.m.rotation=b.m.rotation*quat.eulerAngles(b.rx,b.ry,b.rz)
    

    and when it’s quat * rotation, the rotation axis is global, meaning the x axis is left/right, y axis is up/down, and z axis is front/back and doesn’t matter how the dice is rotated. When it’s rotation * quat, the axis rotation is local and follows the dice. I didn’t know that so thanks for the info.

  • yes, one way or the other. I always have to try it.

    Last I looked my iPad hadn't crashed but I'll report next time I'm in that room.

  • dave1707dave1707 Mod
    Posts: 8,976

    @RonJeffries How much memory is on your iPad. Mine is 64 GB, so that might have something to do with it. I’ll have to try on my iPad Pro 128GB.

  • it's weird. basically you just hold your nose and do it.

    Hmm.

    The blog post that I linked above, https://loopspace.mathforge.org/HowDidIDoThat/Codea/Quaternions/, is my attempt to explain how quaternions relate to rotations. I'd recommend it if you want to really understand what quaternions are. (The one bit I didn't cover was gimbal lock which isn't quite what is described above, but to be honest it's unlikely you'll bump into that.)

    But if you don't care about understanding and just want to use, then you don't need to care about the detail of the implementation and you should just think about rotations.

    Probably the most important fact is that when you apply rotations then the order in which you do so matters. This is actually normal behaviour for applying transformations:

    scale(5)
    translate(100,0)
    

    has a different effect to

    translate(100,0)
    scale(5)
    

    My VecExt code extends the quat built-in extensively to cover all sorts of situations.

  • dave1707dave1707 Mod
    edited December 2020 Posts: 8,976

    @RonJeffries Just ran my above code on the iPad Pro 128GB and it crashed at just above memory of 1,400,000 . There were several times where it paused for several seconds, once around 5 seconds. On the iPad Air 64Gb, it creased around 900,000.

  • dave1707dave1707 Mod
    Posts: 8,976

    @LoopSpace I just saved your link above to my homepage so I’ll be able to look thru it easier. Maybe it will help me understand it more.

  • @dave1707 Happy to answer follow-up questions. Also, I'd say that if one section feels like it's getting a bit bogged down, try skipping to the next part.

  • it did crash, confirmed.

  • @LoopSpace your article is masterful. when i was earning my degrees in math, i'd have loved it. I do think it requires rather a bit of mathematical sophistication to appreciate it. what's nice about quaternions is that folks can use them successfully without looking under the covers.

    good work!

  • @dave1707 the one i tested on is also 128. how much program/data memory, i don't know how to find out/

  • what's nice about quaternions is that folks can use them successfully without looking under the covers.

    Absolutely! I think that one of the fantastic things about code is that you can effectively start "in the middle" with regard to understanding - you can write some decent code without needing to understand a topic in depth - but as you develop it further then you are drawn in to a deeper understanding.

    So I'm happy to help people understand quaternions at a little deeper level than they currently have, but I'm also very conscious that it's best to proceed in small steps. This is my area of Mathematics and it's very easy for me to forget just how weird it can look when it is new.

  • dave1707dave1707 Mod
    Posts: 8,976

    @RonJeffries To see how much memory is being used, use collectgarbage(“count”). I have that in the draw function of the multiple dice program. It displays memory usage at the top center of the screen. You can do a google search for collectgarbage to see different options.

Sign In or Register to comment.