Fixing "disappearing_sliver" bug.
[xonotic/netradiant.git] / regression_tests / q3map2 / disappearing_sliver / README.txt
1 DESCRIPTION OF PROBLEM:
2 =======================
3
4 The example map, maps/disappearing_sliver.map, contains an example of this bug.
5 There are 6 walls in the map, and one tall thin triangular sliver brush in the
6 middle of the room (7 brushes total).  Only one face of the sliver in the
7 middle of the room is a draw surface.  The bug is that this sliver surface is
8 not rendered in the compiled BSP.  Note that the sliver brush was hand-crafted
9 to demonstrate the bug.  If you re-save the map, Radiant might adjust the
10 order in which the planes on the brush are defined, and this might, as a side
11 effect, get rid of the immediate bug.
12
13 To trigger the bug, compile the map; you don't need -vis or -light.  Only
14 -bsp (the first q3map2 stage) is necessary to trigger the bug.  The only
15 entities in the map are 2 lights and a single info_player_deathmatch, so the
16 map will compile for any Q3 mod.
17
18
19 SOLUTION TO PROBLEM:
20 ====================
21
22 Several days were spent studying this problem in great detail.
23
24 The fix for this problem was to make the outcome of the VectorNormalize()
25 function libs/mathlib/mathlib.c more accurate.  The previous code in this
26 function looks something like this:
27
28   vec_t length, ilength; // vec_t is a typedef for 32 bit float.
29
30   /* Compute length */
31
32   ilength = 1.0f/length;
33   out[0] = in[0]*ilength;
34   out[1] = in[1]*ilength;
35   out[2] = in[2]*ilength;
36
37 As you can see, we are introducing a lot of extra error into our normalized
38 vector by multiplying by the reciprocal length instead of outright dividing
39 by the length.  The new fixed code looks like this:
40
41   out[0] = in[0]/length;
42   out[1] = in[1]/length;
43   out[2] = in[2]/length;
44
45 And we get rid of the recpirocal length ilength altogether.  Even the
46 slightest math errors are magnified in successive calls to linear algebra
47 functions.
48
49
50 POSSIBLE SIDE EFFECTS:
51 ======================
52
53 The only negative side effect is that compilation of a map might take longer
54 due to an increased number of divide operations.  (I'm actually not sure if
55 that is indeed the case.)  Another side effect might be that if you're used
56 to a map being broken (missing triangles) or having "sparklies" between
57 brushes, those might be gone now.  :-)
58
59
60 IN-DEPTH DISCUSSION:
61 ====================
62
63 VectorNormalize() is used very frequently in Radiant and tools code.  My goal
64 for this fix was to make the least amount of code change but still be able to
65 demonstrate a significant improvement in math accuracy (including a fix to
66 the test case).  At the same time don't risk that any other bugs pop up as a
67 side effect of this change.
68
69 Here is the sequence of calls (stack trace) that cause the example bug to
70 happen:
71
72   main() in main.c -->
73     BSPMain() in bsp.c -->
74       LoadMapFile() in map.c -->
75         ParseMapEntity() in map.c -->
76           ParseBrush() in map.c -->
77             FinishBrush() in map.c -->
78               CreateBrushWindings() in brush.c -->
79                 ChopWindingInPlace() in polylib.c
80
81 What basically happens in this sequence of calls is that a brush is parsed
82 out of the map file, "infinite" planes are created for each brush face, and
83 then the planes are "intersected" to find the exact vertex topologies of each
84 brush face.  The vertex topology of the visible face of the sliver (in the
85 example map) gets computed with a significant amount of math error.  If we
86 did our math with infinite precision, the sliver face would have the following
87 vertices:
88
89   (67 -1022 0)
90   (88 -892 -768)
91   (134 -1015 0)
92
93 In fact, if you open the map file (disappearing_sliver.map), you can actually
94 see these exact points embedded in the third plane defined on brush 0.
95
96 I managed to print out the actual computed vertices of the sliver face before
97 and after this bug fix.  Basically this is printed out after all the
98 ChopWindingInPlace() calls in the above stack trace:
99
100   (66.984695 -1021.998657 0.000000)
101   (87.989571 -891.969116 -768.174316)
102   (133.998917 -1014.997314 0.000000)
103
104 (If you want to print this out for yourself, print out the coordinates of the
105 winding_t "w" parameter right after the ChopWindingInPlace() call in
106 CreateBrushWindings() in brush.c.)
107
108 The same vertices after the bugfix have the following coordinates:
109
110   (67.000229 -1021.998657 0.000000)
111   (88.000175 -891.999146 -767.997437)
112   (133.999146 -1014.998779 0.000000)
113
114 As you can see, the vertices after the fix are substantially more accurate,
115 and all it took was an improvement to VectorNormalize().
116
117 The problem before the fix was the Z coordinate of the second point, namely
118 -768.174316.  There is a lot of "snap to nearest 1/8 unit" and "epsilon 0.1"
119 code used throughout q3map2.  0.174 is greater than the 0.1 epsilon, and that
120 is the problem.
121
122   main() in main.c -->
123     BSPMain() in bsp.c -->
124       ProcessModels() in bsp.c -->
125         ProcessWorldModel() in bsp.c -->
126           ClipSidesIntoTree() in surface.c -->
127             ClipSideIntoTree_r() in surface.c -->
128               ClipWindingEpsilon() in polylib.c
129
130 Now what ClipWindingEpsilon() does is, since -768.174316 reaches below the
131 plane z = -768 (and over the 0.1 epsilon), it clips the winding_t and creates
132 two points where there used to be only one.
133
134   main() in main.c -->
135     BSPMain() in bsp.c -->
136       ProcessModels() in bsp.c -->
137         ProcessWorldModel() in bsp.c
138           FixTJunctions() in tjunction.c
139             FixBrokenSurface() in tjunction.c
140
141 FixBrokenSurface() realizes that there are two points very close together
142 (in our case, since they were "snapped", the are coincident in fact).
143 Therefore it reports the surface to be broken.  The drawable surface is
144 deleted as a result.
145
146
147 RELATED BUGS:
148 =============
149
150 A lot of the math operations in the Radiant codebase cut corners like this
151 example demonstrates.  There is a lot more code like this that can be
152 improved upon.  In fact, it may make sense to use 64 bit floating points in
153 some important math operations (and then convert back to 32 bit for the return
154 values).  Plans are to look at similar code and improve it.
155
156 The following "issue" was discovered while doing research for this bug.
157 If FixBrokenSurface() sees two points very close together, it attempts to
158 partially fix the problem (the code is not complete) and then returns false,
159 which means that the surface is broken and should not be used.  So in fact
160 it attempts to fix the problem partially but none of the fixes are used.
161 It seems that FixBrokenSurface() should be fixed to completely fix the case
162 where there are two close points, and should report the surface as fixed.
163 This might be a destabilizing change however, so if this is indeed fixed, it
164 may make sense to activate the fix only if a certain flag is set.