Skip to content

Commit 5b829ff

Browse files
committed
Added float quantization system
1 parent d5a9d21 commit 5b829ff

File tree

10 files changed

+612
-28
lines changed

10 files changed

+612
-28
lines changed
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
###############################################################################
2+
# Copyright (c) 2019-2020 Yuri Sarudiansky
3+
#
4+
# Permission is hereby granted, free of charge, to any person obtaining a copy
5+
# of this software and associated documentation files (the "Software"), to deal
6+
# in the Software without restriction, including without limitation the rights
7+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
# copies of the Software, and to permit persons to whom the Software is
9+
# furnished to do so, subject to the following conditions:
10+
#
11+
# The above copyright notice and this permission notice shall be included in
12+
# all copies or substantial portions of the Software.
13+
#
14+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
# THE SOFTWARE.
21+
###############################################################################
22+
23+
# The floating point quantization code was adapted. The original code was taken from
24+
# the Game Engine Architecture book by Jason Gregory
25+
26+
extends Reference
27+
class_name Quantize
28+
29+
const ROTATION_BOUNDS: float = 0.707107
30+
31+
# Define some masks to help pack/unpack quantized rotation quaternions into integers
32+
const MASK_A_9BIT: int = 511 # 511 = 111111111
33+
const MASK_B_9BIT: int = 511 << 9
34+
const MASK_C_9BIT: int = 511 << 18
35+
const MASK_INDEX_9BIT: int = 3 << 27
36+
const MASK_SIGNAL_9BIT: int = 1 << 30
37+
38+
39+
const MASK_A_10BIT: int = 1023 # 1023 = 1111111111
40+
const MASK_B_10BIT: int = 1023 << 10
41+
const MASK_C_10BIT: int = 1023 << 20
42+
const MASK_INDEX_10BIT: int = 3 << 30
43+
44+
const MASK_A_15BIT: int = 32767
45+
const MASK_B_15BIT: int = 32767 << 15
46+
# The C component is packed into a secondary integer but having dedicated
47+
# constant may help reduce confusion when reading the code
48+
const MASK_C_15BIT: int = 32767
49+
const MASK_INDEX_15BIT: int = 3 << 30
50+
# When packing compressed quaternion data using 15 bits per component, one
51+
# bit becomes "wasted". While not entirely necessary, the system here uses
52+
# that bit to restore the signals of the original quaternion in case those
53+
# got flipped because the largest component were negative.
54+
const MASK_SIGNAL_15BIT: int = 1 << 15
55+
56+
57+
# Quantize a unit float (range [0..1]) into an integer of the specified number of bits)
58+
static func quantize_unit_float(val: float, numbits: int) -> int:
59+
# Number of bits cannot exceed 32 bits
60+
assert(numbits <= 32 && numbits > 0)
61+
62+
var intervals: int = 1 << numbits
63+
var scaled: float = val * (intervals - 1)
64+
var rounded: int = int(floor(scaled + 0.5))
65+
66+
if (rounded > intervals - 1):
67+
rounded = intervals - 1
68+
69+
return rounded
70+
71+
static func restore_unit_float(quantized: int, numbits: int) -> float:
72+
assert(numbits <= 32 && numbits > 0)
73+
74+
var intervals: int = 1 << numbits
75+
var intervalsize: float = 1.0 / (intervals - 1)
76+
var approxfloat: float = float(quantized) * intervalsize
77+
78+
79+
return approxfloat
80+
81+
# Quantize a float in the range [minval..maxval]
82+
static func quantize_float(val: float, minval: float, maxval: float, numbits: int) -> int:
83+
var unitfloat: float = (val - minval) / (maxval - minval)
84+
var quantized: int = quantize_unit_float(unitfloat, numbits)
85+
86+
return quantized
87+
88+
# Restore float in arbitrary range
89+
static func restore_float(quantized: int, minval: float, maxval: float, numbits: int) -> float:
90+
var unitfloat: float = restore_unit_float(quantized, numbits)
91+
var val: float = minval + (unitfloat * (maxval - minval))
92+
93+
return val
94+
95+
96+
# Compress the given rotation quaternion using the specified number of bits per component
97+
# using the smallest three method. The returned dictionary contains 5 fields:
98+
# a, b, c -> the smallest three quantized components
99+
# index -> the index [0..3] of the dropped (largest) component
100+
# sig -> The "signal" of the dropped component (1.0 if >= 0, -1.0 if negative)
101+
# Note: Signal is not exactly necessary, but is provided just so if there is any desire to encode it
102+
static func compress_rotation_quat(q: Quat, numbits: int) -> Dictionary:
103+
# Unfortunately it's not possible to directly iterate through the quaternion's components
104+
# using a loop, so create a temporary array to store them
105+
var aq: Array = [q.x, q.y, q.z, q.w]
106+
var mindex: int = 0 # Index of largest component
107+
var mval: float = -1.0 # Largest component value
108+
var sig: float = 1.0 # "Signal" of the dropped component
109+
110+
# Locate the largest component, storing its absolute value as well as the index
111+
# (0 = x, 1 = y, 2 = z and 3 = w)
112+
for i in 4:
113+
var abval: float = abs(aq[i])
114+
115+
if (abval > mval):
116+
mval = abval
117+
mindex = i
118+
119+
if (aq[mindex] < 0.0):
120+
sig = -1.0
121+
122+
# Drop the largest component
123+
aq.erase(aq[mindex])
124+
125+
# Now loop again through the components, quantizing them
126+
for i in 3:
127+
var fl: float = aq[i] * sig
128+
aq[i] = quantize_float(fl, -ROTATION_BOUNDS, ROTATION_BOUNDS, numbits)
129+
130+
131+
return {
132+
"a": aq[0],
133+
"b": aq[1],
134+
"c": aq[2],
135+
"index": mindex,
136+
"sig": 1 if (sig == 1.0) else 0,
137+
}
138+
139+
140+
# Restore the rotation quaternion. The quantized values must be given in a dictionary with
141+
# the same format of the one returned by the compress_rotation_quat() function.
142+
static func restore_rotation_quat(quant: Dictionary, numbits: int) -> Quat:
143+
# Take the signal (just a multiplier)
144+
var sig: float = 1.0 if quant.sig == 1 else -1.0
145+
146+
# Restore components a, b and c
147+
var ra: float = restore_float(quant.a, -ROTATION_BOUNDS, ROTATION_BOUNDS, numbits) * sig
148+
var rb: float = restore_float(quant.b, -ROTATION_BOUNDS, ROTATION_BOUNDS, numbits) * sig
149+
var rc: float = restore_float(quant.c, -ROTATION_BOUNDS, ROTATION_BOUNDS, numbits) * sig
150+
# Restore the dropped component
151+
var dropped: float = sqrt(1.0 - ra*ra - rb*rb - rc*rc) * sig
152+
153+
var ret: Quat = Quat()
154+
155+
match quant.index:
156+
0:
157+
# X was dropped
158+
ret = Quat(dropped, ra, rb, rc)
159+
160+
1:
161+
# Y was dropped
162+
ret = Quat(ra, dropped, rb, rc)
163+
164+
2:
165+
# Z was dropped
166+
ret = Quat(ra, rb, dropped, rc)
167+
168+
3:
169+
# W was dropped
170+
ret = Quat(ra, rb, rc, dropped)
171+
172+
return ret
173+
174+
175+
176+
# Compress the given rotation quaternion using 9 bits per component. This is a "wrapper"
177+
# function that packs the quantized value into a single integer. Because there is still
178+
# some "room" (only 29 bits of the 32 are used), the original signal of the quaternion is
179+
# also stored, meaning that it can be restored.
180+
static func compress_rquat_9bits(q: Quat) -> int:
181+
# Compress the components using the generalized Quat compression
182+
var c: Dictionary = compress_rotation_quat(q, 9)
183+
return ( ((c.sig << 30) & MASK_SIGNAL_9BIT) |
184+
((c.index << 27) & MASK_INDEX_9BIT) |
185+
((c.c << 18) & MASK_C_9BIT) |
186+
((c.b << 9) & MASK_B_9BIT) |
187+
(c.a & MASK_A_9BIT) )
188+
189+
# Restores a quaternion that was previously quantized into a single integer using 9 bits
190+
# per component. In this case the original signal of the quaternion will be restored.
191+
static func restore_rquat_9bits(compressed: int) -> Quat:
192+
var unpacked: Dictionary = {
193+
"a": compressed & MASK_A_9BIT,
194+
"b": (compressed & MASK_B_9BIT) >> 9,
195+
"c": (compressed & MASK_C_9BIT) >> 18,
196+
"index": (compressed & MASK_INDEX_9BIT) >> 27,
197+
"sig": (compressed & MASK_SIGNAL_9BIT) >> 30,
198+
}
199+
200+
return restore_rotation_quat(unpacked, 9)
201+
202+
203+
# Compress the given rotation quaternion using 10 bits per component. This is a "wrapper"
204+
# function that packs the quantized values into a single integer. Note that in this case
205+
# the restored quaternion may be entirely "flipped" as the original signal cannot be
206+
# stored within the packed integer.
207+
static func compress_rquat_10bits(q: Quat) -> int:
208+
# Compress the components using the generalized function
209+
var c: Dictionary = compress_rotation_quat(q, 10)
210+
return ( ((c.index << 30) & MASK_INDEX_10BIT) |
211+
((c.c << 20) & MASK_C_10BIT) |
212+
((c.b << 10) & MASK_B_10BIT) |
213+
(c.a & MASK_A_10BIT) )
214+
215+
# Restores a quaternion that was previously quantized into a single integer using 10 bits
216+
# per component. In this case the original signal may not be restored.
217+
static func restore_rquat_10bits(c: int) -> Quat:
218+
# Unpack the components
219+
var unpacked: Dictionary = {
220+
"a": c & MASK_A_10BIT,
221+
"b": (c & MASK_B_10BIT) >> 10,
222+
"c": (c & MASK_C_10BIT) >> 20,
223+
"index": (c & MASK_INDEX_10BIT) >> 30,
224+
"sig": 1, # Use 1.0 as multiplier because the signal cannot be restored in this case
225+
}
226+
227+
return restore_rotation_quat(unpacked, 10)
228+
229+
230+
# Compress the given rotation quaternion using 15 bits per component. This is a "wrapper"
231+
# function that packs the quantized values into two intergers (using PoolIntArray). In
232+
# memory thsi will still use the full range of the integer values, but the second entry in
233+
# the returned array can safely discard 16 bits, which is basically the desired usage when
234+
# sending data through network. Note that in this case, using a full 32 bit + 16 bit leaves
235+
# room for a single bit, which is used to encode the original quaternion signal.
236+
static func compress_rquat_15bits(q: Quat) -> PoolIntArray:
237+
# Obtain the compressed data
238+
var c: Dictionary = compress_rotation_quat(q, 15)
239+
240+
# Pack the first element of the array - contains index, A and B elements
241+
var packed0: int = ( ((c.index << 30) & MASK_INDEX_15BIT) |
242+
((c.b << 15) & MASK_B_15BIT) |
243+
(c.a & MASK_A_15BIT) )
244+
245+
# Pack the second element of the array - contains signal and C element
246+
var packed1: int = (((c.sig & MASK_SIGNAL_15BIT) << 15) | (c.c & MASK_C_15BIT))
247+
248+
return PoolIntArray([packed0, packed1])
249+
250+
# Restores a quaternion compressed using 15 bits per component. The input must be integers
251+
# within the PoolIntArray of the compression function, in the same order for the arguments.
252+
static func restore_rquat_15bits(pack0: int, pack1: int) -> Quat:
253+
# Unpack the elements
254+
var unpacked: Dictionary = {
255+
"a": pack0 & MASK_A_15BIT,
256+
"b": (pack0 & MASK_B_15BIT) >> 15,
257+
"c": pack1 & MASK_C_15BIT,
258+
"index": (pack0 & MASK_INDEX_15BIT) >> 30,
259+
"sig": (pack1 & MASK_SIGNAL_15BIT) >> 15
260+
}
261+
262+
return restore_rotation_quat(unpacked, 15)
263+
264+

changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
Some smaller commits related to minor fixes (specially comment corrections) are not going to be listed here.
22

3+
#### 2020 May 20
4+
* Added a new "sub-addon" into the General addon directory, `quantize.gd`. It provides means to quantize floating point numbers as well as compression of rotation quaternions using the smallest three method. Tutorial (http://kehomsforge.com/tutorials/multi/GodotAddonPack) has been updated.
5+
36
#### 2020 May 15
47
* (Network) Replicated floating point numbers (even compound ones like Vector2, Vector3 etc) can use tolerance to compare them. Tutorial (http://kehomsforge.com/tutorials/multi/GodotAddonPack) has been updated to show how to use this (topic `Floating Point Comparison`)
58

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[gd_resource type="ButtonGroup" format=2]
2+
3+
[resource]

0 commit comments

Comments
 (0)