mirror of
https://github.com/UberGames/GtkRadiant.git
synced 2024-11-28 14:42:02 +00:00
f7764a84b2
A regression test is included. This is a bug in q3map2. Please read regression_tests/q3map2/disappearing_sliver/README.txt for more info. git-svn-id: svn://svn.icculus.org/gtkradiant/GtkRadiant/trunk@363 8a3a26a2-13c4-0310-b231-cf6edde360e5
164 lines
6.5 KiB
Text
164 lines
6.5 KiB
Text
DESCRIPTION OF PROBLEM:
|
|
=======================
|
|
|
|
The example map, maps/disappearing_sliver.map, contains an example of this bug.
|
|
There are 6 walls in the map, and one tall thin triangular sliver brush in the
|
|
middle of the room (7 brushes total). Only one face of the sliver in the
|
|
middle of the room is a draw surface. The bug is that this sliver surface is
|
|
not rendered in the compiled BSP. Note that the sliver brush was hand-crafted
|
|
to demonstrate the bug. If you re-save the map, Radiant might adjust the
|
|
order in which the planes on the brush are defined, and this might, as a side
|
|
effect, get rid of the immediate bug.
|
|
|
|
To trigger the bug, compile the map; you don't need -vis or -light. Only
|
|
-bsp (the first q3map2 stage) is necessary to trigger the bug. The only
|
|
entities in the map are 2 lights and a single info_player_deathmatch, so the
|
|
map will compile for any Q3 mod.
|
|
|
|
|
|
SOLUTION TO PROBLEM:
|
|
====================
|
|
|
|
Several days were spent studying this problem in great detail.
|
|
|
|
The fix for this problem was to make the outcome of the VectorNormalize()
|
|
function libs/mathlib/mathlib.c more accurate. The previous code in this
|
|
function looks something like this:
|
|
|
|
vec_t length, ilength; // vec_t is a typedef for 32 bit float.
|
|
|
|
/* Compute length */
|
|
|
|
ilength = 1.0f/length;
|
|
out[0] = in[0]*ilength;
|
|
out[1] = in[1]*ilength;
|
|
out[2] = in[2]*ilength;
|
|
|
|
As you can see, we are introducing a lot of extra error into our normalized
|
|
vector by multiplying by the reciprocal length instead of outright dividing
|
|
by the length. The new fixed code looks like this:
|
|
|
|
out[0] = in[0]/length;
|
|
out[1] = in[1]/length;
|
|
out[2] = in[2]/length;
|
|
|
|
And we get rid of the recpirocal length ilength altogether. Even the
|
|
slightest math errors are magnified in successive calls to linear algebra
|
|
functions.
|
|
|
|
|
|
POSSIBLE SIDE EFFECTS:
|
|
======================
|
|
|
|
The only negative side effect is that compilation of a map might take longer
|
|
due to an increased number of divide operations. (I'm actually not sure if
|
|
that is indeed the case.) Another side effect might be that if you're used
|
|
to a map being broken (missing triangles) or having "sparklies" between
|
|
brushes, those might be gone now. :-)
|
|
|
|
|
|
IN-DEPTH DISCUSSION:
|
|
====================
|
|
|
|
VectorNormalize() is used very frequently in Radiant and tools code. My goal
|
|
for this fix was to make the least amount of code change but still be able to
|
|
demonstrate a significant improvement in math accuracy (including a fix to
|
|
the test case). At the same time don't risk that any other bugs pop up as a
|
|
side effect of this change.
|
|
|
|
Here is the sequence of calls (stack trace) that cause the example bug to
|
|
happen:
|
|
|
|
main() in main.c -->
|
|
BSPMain() in bsp.c -->
|
|
LoadMapFile() in map.c -->
|
|
ParseMapEntity() in map.c -->
|
|
ParseBrush() in map.c -->
|
|
FinishBrush() in map.c -->
|
|
CreateBrushWindings() in brush.c -->
|
|
ChopWindingInPlace() in polylib.c
|
|
|
|
What basically happens in this sequence of calls is that a brush is parsed
|
|
out of the map file, "infinite" planes are created for each brush face, and
|
|
then the planes are "intersected" to find the exact vertex topologies of each
|
|
brush face. The vertex topology of the visible face of the sliver (in the
|
|
example map) gets computed with a significant amount of math error. If we
|
|
did our math with infinite precision, the sliver face would have the following
|
|
vertices:
|
|
|
|
(67 -1022 0)
|
|
(88 -892 -768)
|
|
(134 -1015 0)
|
|
|
|
In fact, if you open the map file (disappearing_sliver.map), you can actually
|
|
see these exact points embedded in the third plane defined on brush 0.
|
|
|
|
I managed to print out the actual computed vertices of the sliver face before
|
|
and after this bug fix. Basically this is printed out after all the
|
|
ChopWindingInPlace() calls in the above stack trace:
|
|
|
|
(66.984695 -1021.998657 0.000000)
|
|
(87.989571 -891.969116 -768.174316)
|
|
(133.998917 -1014.997314 0.000000)
|
|
|
|
(If you want to print this out for yourself, print out the coordinates of the
|
|
winding_t "w" parameter right after the ChopWindingInPlace() call in
|
|
CreateBrushWindings() in brush.c.)
|
|
|
|
The same vertices after the bugfix have the following coordinates:
|
|
|
|
(67.000229 -1021.998657 0.000000)
|
|
(88.000175 -891.999146 -767.997437)
|
|
(133.999146 -1014.998779 0.000000)
|
|
|
|
As you can see, the vertices after the fix are substantially more accurate,
|
|
and all it took was an improvement to VectorNormalize().
|
|
|
|
The problem before the fix was the Z coordinate of the second point, namely
|
|
-768.174316. There is a lot of "snap to nearest 1/8 unit" and "epsilon 0.1"
|
|
code used throughout q3map2. 0.174 is greater than the 0.1 epsilon, and that
|
|
is the problem.
|
|
|
|
main() in main.c -->
|
|
BSPMain() in bsp.c -->
|
|
ProcessModels() in bsp.c -->
|
|
ProcessWorldModel() in bsp.c -->
|
|
ClipSidesIntoTree() in surface.c -->
|
|
ClipSideIntoTree_r() in surface.c -->
|
|
ClipWindingEpsilon() in polylib.c
|
|
|
|
Now what ClipWindingEpsilon() does is, since -768.174316 reaches below the
|
|
plane z = -768 (and over the 0.1 epsilon), it clips the winding_t and creates
|
|
two points where there used to be only one.
|
|
|
|
main() in main.c -->
|
|
BSPMain() in bsp.c -->
|
|
ProcessModels() in bsp.c -->
|
|
ProcessWorldModel() in bsp.c
|
|
FixTJunctions() in tjunction.c
|
|
FixBrokenSurface() in tjunction.c
|
|
|
|
FixBrokenSurface() realizes that there are two points very close together
|
|
(in our case, since they were "snapped", the are coincident in fact).
|
|
Therefore it reports the surface to be broken. The drawable surface is
|
|
deleted as a result.
|
|
|
|
|
|
RELATED BUGS:
|
|
=============
|
|
|
|
A lot of the math operations in the Radiant codebase cut corners like this
|
|
example demonstrates. There is a lot more code like this that can be
|
|
improved upon. In fact, it may make sense to use 64 bit floating points in
|
|
some important math operations (and then convert back to 32 bit for the return
|
|
values). Plans are to look at similar code and improve it.
|
|
|
|
The following "issue" was discovered while doing research for this bug.
|
|
If FixBrokenSurface() sees two points very close together, it attempts to
|
|
partially fix the problem (the code is not complete) and then returns false,
|
|
which means that the surface is broken and should not be used. So in fact
|
|
it attempts to fix the problem partially but none of the fixes are used.
|
|
It seems that FixBrokenSurface() should be fixed to completely fix the case
|
|
where there are two close points, and should report the surface as fixed.
|
|
This might be a destabilizing change however, so if this is indeed fixed, it
|
|
may make sense to activate the fix only if a certain flag is set.
|