Opened 11 years ago

Closed 10 years ago

Last modified 10 years ago

#1987 closed enhancement (invalid)

[PATCH] Better terrain normal calculation

Reported by: sanderd17 Owned by:
Priority: Nice to Have Milestone:
Component: Core engine Keywords: patch
Cc: sanderd17@… Patch:

Description

The following patch contains a diff for better normal calculation. It's better in the following ways:

  • Mathematically equal (go take a piece of paper and work it out ;)
  • 4 square roots less, a number of native operations and comparisons
  • one Query to the terrain less
  • less code in general
  • better precision due to the elimination of the square roots

Attachments (1)

normal.diff (2.7 KB ) - added by sanderd17 11 years ago.

Download all attachments as: .zip

Change History (21)

comment:1 by sanderd17, 11 years ago

Some aditional notes:

  • border cases (within the if-statements in the original code) are handled by the CalcPosition(Fixed) method. There is no reason to check for them twice
  • 46 lines of code less to be exact ;)

comment:2 by sanderd17, 11 years ago

Keywords: patch review added

comment:3 by sanderd17, 11 years ago

Oops, forgot that the vectors n0 - n3 aren't normalised when the mean is calculated. This skews the result.

So it's not mathematically equivalent. But I'd even say the old method is mathematically skewed (for not normalizing the vectors after taking the cross product).

comment:4 by historic_bruno, 11 years ago

Milestone: BacklogAlpha 15

comment:5 by scroogie, 11 years ago

Imho you can take out even more if you respect the regularity of the Grid. (up - down) will lead to 1 component being 0, because they have equal X coordinate, and another component being 2*d where d is the Grid distance. The same goes for left-right. The 2*d factor can be left out, as you normalize anyway, so the normal should be something like (left.Z - right.Z; up.Z - down.Z; 2*d).normalize().

Last edited 11 years ago by scroogie (previous) (diff)

by sanderd17, 11 years ago

Attachment: normal.diff added

comment:6 by sanderd17, 11 years ago

You're right (except for the actual formula, as Y is the up coordinate).

I've updated the patch. Although now it's a bit more code than the previous patch, there are less operations done.

comment:7 by scroogie, 11 years ago

Yeah, I know, I always forget the convention 0ad uses. You could also calculate the height directly, eliminating the 8 multiplications for the X and Z coordinates, but I guess readability is more important than that. :)

Ah and btw, nice find, care to sum up the difference in op count? :)

Last edited 11 years ago by scroogie (previous) (diff)

comment:8 by sanderd17, 11 years ago

Which multiplications?

And counting the ops, that's a bit much. But OK.

A cross product on itself is at least 12 operations (and 5 overflow checks that probably contain some additional operations). A normalize is a length calculation (5 operations + a sqrt) and 3 divisions. So in total, I have 4 cross products (48 ops) 4 normalizes (32 ops + 4 sqrts) + 6 additional ops saved.

And I used 3 ops extra.

So that gives a net profit of at least 83 ops and 4 sqrts per normal calculated.

comment:9 by scroogie, 11 years ago

The op count was just out of curiosity, as it seemed a good gain. Sorry if that bugged you. :)

About the multiplications: As the normal is only dependent on the Y coordinate, you wouldn't need the X and Z coordinates (in calcposition). So you could replace the 4 calls to calcposition and 4 vectors with 4 floats and direct heightmap lookups. Something like this:

+       float lz,rz,uz,dz;
+       i = clamp(i, (ssize_t)0, m_MapSize-1);
+       j = clamp(j, (ssize_t)0, m_MapSize-1);
+       lz = (i>0) ? float(HEIGHT_SCALE * m_Heightmap[j*m_MapSize + (i-1)]) : float(HEIGHT_SCALE * m_Heightmap[j*m_MapSize]);
+       rz = (i<(m_MapSize-1)) ? float(HEIGHT_SCALE * m_Heightmap[j*m_MapSize + i + 1]) : float(HEIGHT_SCALE * m_Heightmap[j*m_MapSize + i]);
+       uz = (j>0) ? float(HEIGHT_SCALE * m_Heightmap[(j-1)*m_MapSize + i]) : float(HEIGHT_SCALE * m_Heightmap[j*m_MapSize+i]);
+       dz = (j<(m_MapSize-1)) ? float(HEIGHT_SCALE * m_Heightmap[(j+1)*m_MapSize + i]) : float(HEIGHT_SCALE * m_Heightmap[j*m_MapSize+i]);
+       normal.X = lz-rz;
+       normal.Y = 2.*float(TERRAIN_TILE_SIZE);
+       normal.Z = uz-dz;
+       normal.Normalize();  

But to be honest I don't know if such a patch would be accepted. I'm not a contributor, I just found this ticket coincidentally and thought my first suggestion would improve your patch.

in reply to:  3 comment:10 by historic_bruno, 11 years ago

Replying to sanderd17:

Oops, forgot that the vectors n0 - n3 aren't normalised when the mean is calculated. This skews the result.

So it's not mathematically equivalent. But I'd even say the old method is mathematically skewed (for not normalizing the vectors after taking the cross product).

So the correct thing to do would be to normalize them to avoid the skewing?

The test_CalcNormal* tests pass with or without the normalization of n0-n3 (maybe it doesn't test cases where that matters, or the error is within the allowed 0.01 delta), but they fail with the attached patch.

Last edited 11 years ago by historic_bruno (previous) (diff)

comment:11 by sanderd17, 11 years ago

Damn, that's right. Sorry, I made a mistake. My method seems to prefer terrain with a bigger slope, so it's not mathematically equivalent. I guess we'll leave it this way.

But now that I'm inspecting it a bit better, shouldn't CCmpTerrain->CalcNormal use a fixed version of the method Terrain->CalcExactNormal? As that method is less CPU intensive (also when ported to fixeds), and it seems to do a better job at calculating the normal (as it takes account of the triangle you're on). That method does almost what I did, a single sqrt, and a single cross product.

Last edited 11 years ago by historic_bruno (previous) (diff)

comment:12 by historic_bruno, 11 years ago

I don't know if one is better or worse, they are different methods of calculating the normal, the same distinction exists for finding terrain height (see GetVertexGroundLevel vs. GetExactGroundLevel)

Last edited 11 years ago by historic_bruno (previous) (diff)

comment:13 by sanderd17, 11 years ago

Well, the CalcNormal from CCmpTerrain gets as arguments coordinates in the world, from those coordinates, it gets the nearest vertex, from which it asks the normal. That normal takes 4 sqrts to calculate with the current method.

While it could also ask the normal on the triangle it's on (CalcExactNormal), which would only take some minor calculations and a single sqrt. And it looks more exact to me.

E.g. when a unit is near a cliff, the nearest vertex can be on the edge of the cliff, if you say the cliff is almost vertical, the normal will point at almost 45°, while the center of the unit might even be on flat terrain, and using the exactNormal calculation, the normal will indeed point up.

This could fix the "Horse legs sticking out" problem you mentioned in #1988. As the horse center point will normally not be on such steep terrain.

comment:14 by historic_bruno, 11 years ago

Priority: Must HaveNice to Have

comment:15 by historic_bruno, 11 years ago

Keywords: review removed

comment:16 by wraitii, 11 years ago

Hadn't noticed that ticket…

Seems good to me but for using CalcPosition, which is about as inefficient as you can get here. Given that only height info is needed, I recommend forgetting all about the rest and using

ssize_t hi = clamp(i, (ssize_t)0, m_MapSize-1);
ssize_t hj = clamp(j, (ssize_t)0, m_MapSize-1);
float leftHeight = float(m_Heightmap[hj*m_MapSize + hi-1]);
float rightHeight = float(m_Heightmap[hj*m_MapSize + hi+1]);
float upHeight = float(m_Heightmap[(hj-1)*m_MapSize + hi]);
float downHeight = float(m_Heightmap[(hj+1)*m_MapSize + hi]);

and then changing whatever.Y to whateverHeight, but the rest is good.

This indeed calculates vertex normal and not surface normal, which is calculated by CalcExactNormal, which could also use some work I believe (mainly because there is a ton of redundancy by calling CalcExactNormal()).

Last edited 11 years ago by wraitii (previous) (diff)

comment:17 by scroogie, 11 years ago

Hi wraitii, I did that in comment #9.

comment:18 by wraitii, 11 years ago

Oh, right, sorry, had only read the patch. Your solution is better than mine because I forgot that this might try to reach wrong/Out of range values.

comment:19 by wraitii, 10 years ago

Resolution: invalid
Status: newclosed

Closing this as it is apparently invalid per Sanderd17's comment.

comment:20 by historic_bruno, 10 years ago

Milestone: Alpha 15
Note: See TracTickets for help on using tickets.