Dear All:
Hope you are having a nice Sunday.
I have decided that, as all the D3DXCreate... functions return vertex normals in addition to vertices themselves, it is more efficient to implement D3DXComputeNormals and use this function rather than calculate normals individually for each shape (or create an internal function that does the same thing).
As you can see from this patch: http://github.com/misha680/wine/commit/0e975e7360f403eb123055b6ff996137e0b22... the normals computed by D3DXComputeNormals are almost equal to those originally returned by D3DXCreateCylinder for complex enough values of the radii, length, slices, and stacks (these values are within 10^-6 of each other).
I have taken the following description from MSDN: "A normal for a vertex is generated by averaging the normals of all faces that share that vertex.
If adjacency is provided, replicated vertices are ignored and 'smoothed' over. If adjacency is not provided, replicated vertices will have normals averaged in from only the faces explicitly referencing them."
and implemented it in a test case (attached - code, compilation script, and compiled executable via MingW).
For a very simplified cylinder (radii 1.0f, length 0.0f, 3 slices, 2 stacks) all seems well, and my normals match perfectly to those created by D3DXComputeNormals.
However, for a more complicated case: static const FLOAT radius = 1.0f; static const FLOAT length = 1.0f; static const UINT slices = 10; static const UINT stacks = 20;
You can see something quite perplexing. Specifically, below you can see all 3 triangles that contain vertex #11 (11, 21, and 12 are vertex indices, below you will see the actual vertex X,Y,Z values, and finally the computed normal for the specific triangle).
Below, you can see under "Averaged Normals", first the normal computed by averaging and normalizing the normals for each of the triangles, and then the original normal computed by D3DXComputeNormals.
Notice that they are quite (significantly) different. This does not happen for a lot of vertices, but for quite a few (say 5-10%).
Index Buffer:
11,21,12, {0,1,-0.5}, {0,1,-0.45}, {0.587785,0.809017,-0.5}, {0.00954915,0.0293893,0}, 20,30,11, {-0.587785,0.809017,-0.5}, {-0.587785,0.809017,-0.45}, {0,1,-0.5}, {-0.00954915,0.0293893,0}, 11,30,21, {0,1,-0.5}, {-0.587785,0.809017,-0.45}, {0,1,-0.45}, {-0.00954915,0.0293893,0},
Averaged Normals:
11 {-0.107677,0.994186,0} {-1.99491e-008,1,0},
I have used the equivalent statement for D3DXComputeNormals:
if (!SUCCEEDED(D3DXComputeTangentFrameEx(mesh, D3DX_DEFAULT, 0, D3DX_DEFAULT, 0, D3DX_DEFAULT, 0, D3DDECLUSAGE_NORMAL, 0, D3DXTANGENT_GENERATE_IN_PLACE | D3DXTANGENT_CALCULATE_NORMALS, NULL, -1.01f, -0.01f, -1.01f, NULL, NULL)))
and tried to vary the parameters listed below, but honestly am a bit stumped...
I will investigate further, but if perhaps I am missing something simple, would be great to know!
Thanks Misha
p.s. Fyi these are the relevant parameters for D3DXComputeTangentFrameEx from http://msdn.microsoft.com/en-us/library/bb172745%28v=VS.85%29.aspx Thank you! Misha
fPartialEdgeThreshold [in] FLOAT
Specifies the maximum cosine of the angle at which two partial derivatives are deemed to be incompatible with each other. If the dot product of the direction of the two partial derivatives in adjacent triangles is less than or equal to this threshold, then the vertices shared between these triangles will be split.
fSingularPointThreshold [in] FLOAT
Specifies the maximum magnitude of a partial derivative at which a vertex will be deemed singular. As multiple triangles are incident on a point that have nearby tangent frames, but altogether cancel each other out (such as at the top of a sphere), the magnitude of the partial derivative will decrease. If the magnitude is less than or equal to this threshold, then the vertex will be split for every triangle that contains it.
fNormalEdgeThreshold [in] FLOAT
Similar to fPartialEdgeThreshold, specifies the maximum cosine of the angle between two normals that is a threshold beyond which vertices shared between triangles will be split. If the dot product of the two normals is less than the threshold, the shared vertices will be split, forming a hard edge between neighboring triangles. If the dot product is more than the threshold, neighboring triangles will have their normals interpolated.
On 12 July 2010 04:48, Misha Koshelev misha680@gmail.com wrote:
Index Buffer:
11,21,12, {0,1,-0.5}, {0,1,-0.45}, {0.587785,0.809017,-0.5}, {0.00954915,0.0293893,0}, 20,30,11, {-0.587785,0.809017,-0.5}, {-0.587785,0.809017,-0.45}, {0,1,-0.5}, {-0.00954915,0.0293893,0}, 11,30,21, {0,1,-0.5}, {-0.587785,0.809017,-0.45}, {0,1,-0.45}, {-0.00954915,0.0293893,0},
Averaged Normals:
11 {-0.107677,0.994186,0} {-1.99491e-008,1,0},
The part of the mesh you're interested in looks like this:
20---11---12 | /| /| |B / |A / | | / C| / | |/ |/ | 30---21---22
The "real" vertex normal at a given vertex would be {x, y, 0} for this shape (for an open cylinder anyway, for a closed cylinder you'd have {0, 0, 1.0} and {0, 0, -1.0} for the vertices from the caps), so the {-1.99491e-008, 1.0, 0.0} d3dx calculates is pretty close.
Vertex 11 is part of the faces A, B and C. Notice that faces B and C (must) have the same face normal. If you simply calculate the average (mean) of the normals for faces A, B and C, the resulting vector would be biased towards B/C. That's what happens with your code. You can mitigate that by assigning weights to the faces based on the area of the triangles and the angle between the edges at the vertex you're interested in. I'd expect d3dx to do something similar to that.
However, note that for simple, regular shapes like these it's *much* simpler to just generate the normals together with the vertices. E.g. for a cylinder, you'd simply have normalize(x, y, 0.0), {0.0, 0.0, 1.0} and {0.0, 0.0, -1.0}, for a sphere you'd have normalize{x, y, z}, etc.
On Mon, 2010-07-12 at 14:21 +0200, Henri Verbeet wrote:
On 12 July 2010 04:48, Misha Koshelev misha680@gmail.com wrote:
Index Buffer:
11,21,12, {0,1,-0.5}, {0,1,-0.45}, {0.587785,0.809017,-0.5}, {0.00954915,0.0293893,0}, 20,30,11, {-0.587785,0.809017,-0.5}, {-0.587785,0.809017,-0.45}, {0,1,-0.5}, {-0.00954915,0.0293893,0}, 11,30,21, {0,1,-0.5}, {-0.587785,0.809017,-0.45}, {0,1,-0.45}, {-0.00954915,0.0293893,0},
Averaged Normals:
11 {-0.107677,0.994186,0} {-1.99491e-008,1,0},
The part of the mesh you're interested in looks like this:
20---11---12 | /| /| |B / |A / | | / C| / | |/ |/ | 30---21---22
The "real" vertex normal at a given vertex would be {x, y, 0} for this shape (for an open cylinder anyway, for a closed cylinder you'd have {0, 0, 1.0} and {0, 0, -1.0} for the vertices from the caps), so the {-1.99491e-008, 1.0, 0.0} d3dx calculates is pretty close.
Vertex 11 is part of the faces A, B and C. Notice that faces B and C (must) have the same face normal. If you simply calculate the average (mean) of the normals for faces A, B and C, the resulting vector would be biased towards B/C. That's what happens with your code. You can mitigate that by assigning weights to the faces based on the area of the triangles and the angle between the edges at the vertex you're interested in. I'd expect d3dx to do something similar to that.
Thank you Henri for the explanation. In fact it seems that this flag is default on D3DXComputeTangentFrameEx:
D3DXTANGENT_WEIGHT_BY_AREA Weight the direction of the computed per-vertex normal or partial derivative vector according to the areas of triangles attached to that vertex. Mutually exclusive with D3DXTANGENT_WEIGHT_EQUAL.
If I specify D3DXTANGENT_WEIGHT_EQUAL, then the normal vectors I calculate are indeed correct.
However, the quirk is that, unless I am doing something wrong, all three triangles actually have the same area (I am using 1/2 of cross product of two vertices in the triangle, which I believe should in fact be correct - see below):
FLOAT ComputeArea(D3DXVECTOR3* p0, D3DXVECTOR3* p1, D3DXVECTOR3* p2) { D3DXVECTOR3 u = *p1 - *p0; D3DXVECTOR3 v = *p2 - *p0; D3DXVECTOR3 out;
D3DXVec3Cross(&out, &u, &v); return 0.5*D3DXVec3Length(&out); }
and resulting areas and vertices:
Vertex Buffer:
11 {0,1,-0.5,-1.99491e-008,1,0}
Index Buffer:
11,21,12, {0,1,-0.5}, {0,1,-0.45}, {0.587785,0.809017,-0.5}, {0.309017,0.951057,0}, Area = 0.0154509, 20,30,11, {-0.587785,0.809017,-0.5}, {-0.587785,0.809017,-0.45}, {0,1,-0.5}, {-0.309017,0.951056,0}, Area = 0.0154509, 11,30,21, {0,1,-0.5}, {-0.587785,0.809017,-0.45}, {0,1,-0.45}, {-0.309017,0.951056,0}, Area = 0.0154509,
Averaged Normals:
0.0154509 {0.309017,0.951057,0} 0.0154509 {-0.309017,0.951056,0} 0.0154509 {-0.309017,0.951056,0} 11 {-0.107677,0.994186,0} {-1.99491e-008,1,0},
Eesh...
(as always relevant code attached for the interested reader)
However, note that for simple, regular shapes like these it's *much* simpler to just generate the normals together with the vertices. E.g. for a cylinder, you'd simply have normalize(x, y, 0.0), {0.0, 0.0, 1.0} and {0.0, 0.0, -1.0}, for a sphere you'd have normalize{x, y, z}, etc.
Thank you. I have made a relevant patch for D3DXCreateCylinder test: http://github.com/misha680/wine/commit/20ed58d7ba39554d2628c00b4268843b089cd... using this simplified version.
I will focus on this simple version for now, as the goal is to implement the shape functions, and not to actually necessarily implement D3DXComputeTangentFrameEx, although that would be quite nice as well.
Thank you Misha
On 12 July 2010 22:43, Misha Koshelev misha680@gmail.com wrote:
Thank you Henri for the explanation. In fact it seems that this flag is default on D3DXComputeTangentFrameEx:
D3DXTANGENT_WEIGHT_BY_AREA Weight the direction of the computed per-vertex normal or partial derivative vector according to the areas of triangles attached to that vertex. Mutually exclusive with D3DXTANGENT_WEIGHT_EQUAL.
Actually, looking at the docs for D3DXComputeTangentFrameEx() a bit more closely, it says in the description for dwOptions that if neither D3DXTANGENT_WEIGHT_BY_AREA nor D3DXTANGENT_WEIGHT_EQUAL is specified it does indeed weight the normals by the the angle of the outgoing edges for that face.
On Mon, 2010-07-12 at 22:54 +0200, Henri Verbeet wrote:
On 12 July 2010 22:43, Misha Koshelev misha680@gmail.com wrote:
Thank you Henri for the explanation. In fact it seems that this flag is default on D3DXComputeTangentFrameEx:
D3DXTANGENT_WEIGHT_BY_AREA Weight the direction of the computed per-vertex normal or partial derivative vector according to the areas of triangles attached to that vertex. Mutually exclusive with D3DXTANGENT_WEIGHT_EQUAL.
Actually, looking at the docs for D3DXComputeTangentFrameEx() a bit more closely, it says in the description for dwOptions that if neither D3DXTANGENT_WEIGHT_BY_AREA nor D3DXTANGENT_WEIGHT_EQUAL is specified it does indeed weight the normals by the the angle of the outgoing edges for that face.
Henri:
I love your polite way of saying RTFM ;)
Anyway you are correct, I have made a patch: http://github.com/misha680/wine/commit/f199b0fba2cdfb2b0987d1b9fac73f3976222...
Next steps I believe are: * Add tests for D3DXCreateCylinder with unequal radii * Implement D3DXCreateCylinder * Implement D3DXComputeNormals
One minor note about the patch - the compute_normal function is adapted from a book, which I have credited in a comment
/* * Adapted from pg. 246, Frank D Luna, "Introduction to 3D Game Programming with Direct X 9.0c: * A Shader Approach (Wordware Game and Graphics Library) [Paperback]," 2006. */
The function differs from the book in the: * name * parameters all on one line rather than separate lines (minor) * use of D3DXVec3Subtract rather than operator- (only available in C++ I believe) * name of output parameter is normal vs out
I believe this should be okay for inclusion of this function. Plus it is quite basic. However, if this is not ok let me know (I honestly can't imagine such a case though...)
Misha
On 13 July 2010 01:11, Misha Koshelev misha680@gmail.com wrote:
I love your polite way of saying RTFM ;)
While it only rarely hurts to read the docs carefully, that wasn't my intention here. I really only noticed that bit myself after you mentioned D3DXTANGENT_WEIGHT_BY_AREA. What isn't entirely clear to me from reading the documentation is if it will weight by both area and opening angle if you pass D3DXTANGENT_WEIGHT_BY_AREA, or just by area.
One minor note about the patch - the compute_normal function is adapted from a book, which I have credited in a comment
/* * Adapted from pg. 246, Frank D Luna, "Introduction to 3D Game Programming with Direct X 9.0c: * A Shader Approach (Wordware Game and Graphics Library) [Paperback]," 2006. */
The function differs from the book in the:
- name
- parameters all on one line rather than separate lines (minor)
- use of D3DXVec3Subtract rather than operator- (only available in C++ I
believe)
- name of output parameter is normal vs out
I believe this should be okay for inclusion of this function. Plus it is quite basic. However, if this is not ok let me know (I honestly can't imagine such a case though...)
Should be ok. Computing a normal vector by taking the cross product of two other vectors is pretty much basic vector math anyway.