Results 1 to 18 of 18

Thread: Bias in setting random rotation

  1. #1
    Join Date
    May 2016
    Posts
    1,072

    Bias in setting random rotation

    Well, THIS is an interesting one. I made an item for the workshop called the Holy Roller. Basically, it take dice, sets a "random" rotation to them, and places them around itself. A little dice roller.

    But somebody pointed out in the comments that the dice rolls WOULDN'T be random. This was their reference they provided: http://mathworld.wolfram.com/SpherePointPicking.html. I did some testing and they were 100% correct. 2 and 5, on the poles of the dice, were heavily biased. I made an automated repeater to keep rolling the dice over and over. These were the results. Here is how I was attempting to set the angles.

    Code:
    xRot = math.random()*360
    yRot = math.random()*360
    zRot = math.random()*360
    object.setRotation({xRot, yRot, zRot})
    So now I am trying to figure out a real solution to this problem. I just don't understand it well enough to solve. I have been asking for help on various math forums, and gotten some very helpful responses, but still don't have this solved. In the mean time, I just cheated and started throwing torque onto the dice after they are rotated. But I was hoping someone with better maths, or that had already solved this problem, could help.

    Thanks

  2. #2
    I really want to know the answer to this now, I don't really understand why you're having this problem and that "Sphere Point Picking" page isn't easy to understand without some higher level Maths knowledge...

    Now I'm wondering whether using the "R" key on a die is even truly random or not...

  3. #3
    Join Date
    May 2016
    Posts
    1,072
    I think the R key is, it just picks a rotation within a certain range (so you don't just spin it like a top) and a toss-up height of a variable amount. That's as random as shaking the dice and rolling them.

    My limited understanding of this is that it basically ties into Gimbal lock. When you try manipulate all 3 angles of a rotation at once you can get some unexpected results. In this case, getting fully random numbers and applying a different one to x/y/z will cause, more often than not, the angle to point one side or the opposite side up. Because that is where the "poles" of the model are.

    There are mathematical ways to distribute it so that doesn't happen. But so far, I don't think I'm going to be able to figure it out. Unless I can find someone to just tell me the exact answer I need, I may not get it. This is my latest failed attempt:

    Code:
    u1 = math.random()
    u2 = math.random()
    u3 = math.random()
    u4 = math.random()
        --w/x/y/z from your example
    q0 = math.sqrt(-2 * math.log(u1)) * math.cos(2 * math.pi * u2)
    q1 = math.sqrt(-2 * math.log(u1)) * math.sin(2 * math.pi * u2)
    q2 = math.sqrt(-2 * math.log(u3)) * math.cos(2 * math.pi * u4)
    q3 = math.sqrt(-2 * math.log(u3)) * math.sin(2 * math.pi * u4)
        --My conversion from w/x/y/z to Euler x/y/z
    xr = math.atan(  (2*(q0*q1+q2*q3)) / (1-2*(q1^2+q2^2))  )
    yr = math.asin(  2*(q0*q2 - q3*q1)  )
    zr = math.atan(  (2*(q0*q3+q1*q2)) / (1-2*(q2^2+q3^2)) 
        --Converting radians to degrees to set the rotation
    rotation = {math.deg(xr), math.deg(yr), math.deg(zr)}

  4. #4
    Join Date
    Jan 2014
    Posts
    986
    I'm using random quaternions (avoids gimbal lock) for the R dice rolling plus the whole physics component as well.

    We unfortunately don't expose rotations as quaternions so you can't use that method (less then helpful I know). The combination of random forces plus rotation will probably be as random as necessary though.

  5. #5
    Join Date
    May 2016
    Posts
    1,072
    Yeah I think that is where it will end. Trying to crunch the math behind the scenes converting quats and Euler is way outside my paygrade haha.

    Thanks for the confirmation though!

  6. #6
    Have you tried using math.atan2? e.g.

    Code:
    xr = math.atan2(  (2*(q0*q1+q2*q3)) , (1-2*(q1^2+q2^2))  )
    yr = math.asin(  2*(q0*q2 - q3*q1)  )
    zr = math.atan2(  (2*(q0*q3+q1*q2)) , (1-2*(q2^2+q3^2)) )
    Difference is that atan only produces results between -90 deg and 90 deg, i.e. only two of the four quadrants. atan2 uses the signs of the numerator and denominator to place the result in the correct quadrant.

  7. #7
    Join Date
    May 2016
    Posts
    1,072
    The second version from the quat -> Euler conversion? Sadly, yes that was one of the things I tried. I tried to copy/paste yours, but this version actually gives me significantly worse distribution than the first conversion method. That was without me converting the result from radians to degrees, because I am honestly not sure which result that conversion is supposed to give me. So just to be sure, I converted the results for each (x/y/z) into degrees at the end to be sure. Bias changed, but persisted.

  8. #8
    It should be converted from radians to degrees. The trigonometric functions return radians but the TTS API expects degrees.

  9. #9
    This is a piece of code which sets properly uniform rotations. You would need to convert it from quaternion to angles if required. Please ignore double/float conversions, it was required due to framework I was writing it in.

    Code:
    		double u1 = Math.random();
    		double u2 = Math.random();
    		double u3 = Math.random();
    		
    		
    		double u1sqrt = Math.sqrt(u1);
    		double u1m1sqrt = Math.sqrt(1-u1);
    		double x = u1m1sqrt *Math.sin(2*Math.PI*u2);
    		double y = u1m1sqrt *Math.cos(2*Math.PI*u2);
    		double z = u1sqrt *Math.sin(2*Math.PI*u3);
    		double w = u1sqrt *Math.cos(2*Math.PI*u3);
    		
    		return new Quaternion((float)x,(float)y,(float)w,(float)z);
    I was running it overnight to generate results of rolls for multiple dice types and it was quite uniform.

  10. #10
    Join Date
    May 2016
    Posts
    1,072
    My understanding is that this implementation of Lua doesn't support quats.

  11. #11
    Then it is just matter of converting x,y,z,w into euler angles - you have posted it few messages above in your example (https://en.wikipedia.org/wiki/Conver...les_Conversion). Trick is how to generate uniform quaternion, conversion is trivial

    Edit:
    Found the original formula
    http://planning.cs.uiuc.edu/node198.html

  12. #12
    Join Date
    May 2016
    Posts
    1,072
    I made an attempt at this. Somewhere I had issues, either in the execution of the conversion or the generation of the whatevers. Not sure. If you want to take a swing at it and test it out, then by all means please do. Or someone else can? I made 20 or 40 dice which would constantly "re-roll" themselves and print their result to chat, about once every second or two. Then I let it run for a while, printing its cumulative results as it went. I always wound up with significant biases

    Unfortunately, I've thrown the towel in. If somebody gets it to work effectively in game, please share it. I don't have a particular use for it currently, but I'm sure others might. And you never know, I may revisit the project in the future.

  13. #13
    Can I get your test program somewhere? I could try to take a stab at solving the bias problem.

  14. #14
    Join Date
    May 2016
    Posts
    1,072
    Sorry, I ended up deleting it in a huff once upon a time haha

  15. #15
    I managed to hack your dice roller ring (learning lua at same time, so took me a moment). I have yet to run rollers for long period of time and results are hard to interpret. For D6 I got
    61 52 61 52 69 45
    and now I'm running it for D8
    50 36 38 47 44 54 41 55
    Hard to say at the moment if it is really fair. I will try to run it overnight for D6 and see if result 6 is still underrepresented (it was 40 1s versus 20 6s at some point).

    Here is the code - I have put it into rotation = randomRotation() and commented out 'lameFix' part for now.
    Code:
    function randomRotation()
    
      local u1 = math.random();
      local u2 = math.random();
      local u3 = math.random();
    
    
      local u1sqrt = math.sqrt(u1);
      local u1m1sqrt = math.sqrt(1-u1);
      local qx = u1m1sqrt *math.sin(2*math.pi*u2);
      local qy = u1m1sqrt *math.cos(2*math.pi*u2);
      local qz = u1sqrt *math.sin(2*math.pi*u3);
      local qw = u1sqrt *math.cos(2*math.pi*u3);
    
    
      local ysqr = qy * qy;
      local t0 = -2.0 * (ysqr + qz * qz) + 1.0;
      local t1 = 2.0 * (qx * qy - qw * qz);
      local t2 = -2.0 * (qx * qz + qw * qy);
      local t3 = 2.0 * (qy * qz - qw * qx);
      local t4 = -2.0 * (qx * qx + ysqr) + 1.0;
    
      if t2 > 1.0 then t2 = 1.0 end
      if t2 < -1.0 then ts = -1.0 end
    
      local xr = math.asin(t2);
      local yr = math.atan2(t3, t4);
      local zr = math.atan2(t1, t0);
    
      return {math.deg(xr),math.deg(yr),math.deg(zr)}
    end

  16. #16
    Join Date
    May 2016
    Posts
    1,072
    I tried something similar before (3 random numbers into 4 factors converted into X/Y/Z degrees. But I'm sure I was doing something wrong (likely failing to convert into degrees)

    It looks pretty solid. I'm hopeful you may have cracked it. Nice =)

  17. #17
    1151 1174 1185 1192 1188 1178
    I think that numbers look pretty ok. Entire of your modified code follows (sorry for lack of idiomatic lua in modifications, but I learned it yesterday evenining)

    Code:
    --[[    Holy Roller: by MrStump
    
        Instructions for edit:
            radius
                This is how far away from the tool the dice will fall.
                If you set it too low, items will fall back into the bag FOREVER. Fun.
            height
                This is how high off the table, relative to the tool, the dice will fall.
                If you set it too low (negative numbers), dice can spawn in the table. Bad.
            print
                This decides if the results of the dice will print to chat. Will not work with custom dice.
                Can be true or false. True prints, false does not print. ]]--
    
    radius = 4
    height = 4
    printResultsToChat = true
    
    results = {0,0,0,0,0,0,0,0}
    
    
    --[[    End of edit section. below is the functional code, commented.    ]]--
    
    --[[Runs any time an object touches the bag (or it touches anything)]]
    function onCollisionEnter()
        --Gets # of objects in bag. If there are some, it runs the emptyContents function
        if self.getQuantity() > 0 then
            allDice = {}
            emptyContents()
        end
    end
    
    --[[Empties the contents of the bag into a ring around the object.]]
    function emptyContents()
        --Establish some necessary basic information on what is in the bag and where it is
        local contents = self.getObjects()
        local selfRot = self.getRotation()
        local selfPos = self.getPosition()
    
        --Establishes the X/Y/Z of position, and the Y of rotation.
        local yRot = selfRot.y
        local xPos = selfPos.x
        local yPos = selfPos.y
        local zPos = selfPos.z
        --Establishes the variable we need for each spoke (placed item). Like spokes on a wheel.
        --360 is our number of degrees, #contents is our number of individual spokes we will have.
        local spokes = 360/#contents
    
        --Loop, runs through each item in the bag, sets its position and rotation for its destination, and pulls it out
        for i, v in pairs(contents) do
            --This is how we determine the position of each individual removed item. (see end for more)
            local xp = xPos + math.sin( ((spokes*i)+yRot)*0.0174532 ) * radius
            local yp = yPos + height
            local zp = zPos + math.cos( ((spokes*i)+yRot)*0.0174532 ) * radius
            --We place our x/y/z for position into this table. We also place 3 random numbers for rotation X/Y/Z
            takeParam = {
                position = {xp,yp,zp},
                --rotation = {math.random(1,360),math.random(1,360),math.random(1,360)},
                rotation = randomRotation(),
                callback = 'lameFix'
            }
            --Use the info in the table to remove the item.
            local die = self.takeObject(takeParam)
            allDice[i] = die
        end
        --Activates a timer to call a function to print the dice values
        if printResultsToChat == true then
            --destroy makes sure a currently existing one is gone, in case timers overlap.
            Timer.destroy(self.getGUID())
            local timerParams = {
                identifier=self.getGUID(), function_name='printResults',
                function_owner=self, delay=3
            }
            Timer.create(timerParams)
        end
    end
    
    --[[A fix to the bias towards 2/5. This puts torque on the dice, effectively rolling them.]]
    function lameFix(dieObject)
       --[[  dieObject.addTorque( {math.random()*360, math.random()*360, math.random()*360} ) ]]
    end
    
    --[[Activated by the timer if printResultsToChat is true]]
    function printResults()
        local printString = dropper .. ' rolled: '
        for i, v in pairs(allDice) do
            printString = printString .. v.getValue() .. '  '
            results[v.getValue()] = results[v.getValue()]+1
            local p = self.getPosition()
            p["y"] = p["y"] + i
            v.setPositionSmooth(p)
        end
    
        --[[ printToAll(printString, {1,1,1}) ]]
        print(results[1] .. ' ' .. results[2].. ' ' .. results[3].. ' ' .. results[4].. ' ' .. results[5] .. ' ' .. results[6] )
        -- print(results[1] .. ' ' .. results[2].. ' ' .. results[3].. ' ' .. results[4].. ' ' .. results[5] .. ' ' .. results[6] .. ' ' .. results[7] .. ' ' .. results[8])
    
    end
    
    --[[Tracks every object drops and adds it to a table. Needs]]--
    function onObjectDropped(col, obj)
        if printResultsToChat == true then
            dropper = Player[col].steam_name
        end
    end
    
    function randomRotation()
    
      local u1 = math.random();
      local u2 = math.random();
      local u3 = math.random();
    
    
      local u1sqrt = math.sqrt(u1);
      local u1m1sqrt = math.sqrt(1-u1);
      local qx = u1m1sqrt *math.sin(2*math.pi*u2);
      local qy = u1m1sqrt *math.cos(2*math.pi*u2);
      local qz = u1sqrt *math.sin(2*math.pi*u3);
      local qw = u1sqrt *math.cos(2*math.pi*u3);
    
    
      local ysqr = qy * qy;
    	local t0 = -2.0 * (ysqr + qz * qz) + 1.0;
    	local t1 = 2.0 * (qx * qy - qw * qz);
    	local t2 = -2.0 * (qx * qz + qw * qy);
    	local t3 = 2.0 * (qy * qz - qw * qx);
    	local t4 = -2.0 * (qx * qx + ysqr) + 1.0;
    
    	if t2 > 1.0 then t2 = 1.0 end
    	if t2 < -1.0 then ts = -1.0 end
    
    	local xr = math.asin(t2);
    	local yr = math.atan2(t3, t4);
    	local zr = math.atan2(t1, t0);
    
      return {math.deg(xr),math.deg(yr),math.deg(zr)}
    end
    
    
    --[[The core of determining direction from an object, based on its rotation, is based in trig.
    I barely understand it, but I might be able to explain how to use it (if that is what you are here for).
        x = sourceXposition + math.sin( (0+sourceYrotation)*0.0174532)  * distance
        y = yPosition
        z = sourceZposition + math.cos( (0+sourceYrotation)*0.0174532)  * distance
    This is the heart of the formula. Let me explain each part of x.
    
        sourceXposition || the halo's x position.
            When we're giving our placed item coordinates, they will be relative to the world. Where 0,0,0 is the middle of the table.
            Basically, if we didn't add sourceXposition, we would be making a ring of objects around table center instead of this object.
        math.sin( (0 + sourceYrotation) * 0.017453 ) || converting an angle into a vector
            math.sin() is our mathmatical function. for Z, you will notice we use math.cos() instead, and the z position, but the rest is the same.
            sourceYrotation is the angle we are looking to convert into a vector (for our item to be placed)
            0 is how much we want to add to our angle. If 0 comes out the left side, 90 would come out the top, 180 out the right, etc
            0.0174532 is the number used to conver radians/degrees to/from eachother. Dont try to change this number.
        distance || measurement of distance
            How far from our item's center we want to spawn the object.
    
    If you cam here looking to understand how this works, I hope this helped. ]]--

  18. #18
    Join Date
    May 2016
    Posts
    1,072
    Very nicely done =)

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •