Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix oct-encoded normal upsampling. #1961

Merged
merged 12 commits into from
Jul 27, 2014
70 changes: 68 additions & 2 deletions Source/Workers/upsampleQuantizedTerrainMesh.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*global define*/
define([
'../Core/BoundingSphere',
'../Core/Cartesian2',
'../Core/Cartesian3',
'../Core/Cartographic',
'../Core/defined',
Expand All @@ -11,6 +12,7 @@ define([
'./createTaskProcessorWorker'
], function(
BoundingSphere,
Cartesian2,
Cartesian3,
Cartographic,
defined,
Expand Down Expand Up @@ -372,18 +374,82 @@ define([
return CesiumMath.lerp(this.first.getV(), this.second.getV(), this.ratio);
};

function signNotZero(v) {
return v < 0.0 ? -1.0 : 1.0;
}

function toSNorm(f) {
return Math.round((f * 0.5 + 0.5) * 255.0);
}

function octDecode(x, y, result) {
result.x = x / 255.0 * 2.0 - 1.0;
result.y = y / 255.0 * 2.0 - 1.0;
result.z = 1.0 - (Math.abs(result.x) + Math.abs(result.y));

if (result.z < 0.0)
{
var oldVX = x / 255.0 * 2.0 - 1.0;
result.x = (1.0 - Math.abs(result.y)) * signNotZero(oldVX);
result.y = (1.0 - Math.abs(oldVX)) * signNotZero(result.y);
}

return Cartesian3.normalize(result, result);
}

function octEncode(vector, out) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make this a generic unit-tested function (even if it is @private) since we are going to want it for the geometry pipeline, which we are going to start work on again soon for KML? Perhaps it is a function Cartesian3? Or perhaps it is better to make a new oct class and put static decode and encode functions in it.

signNotZero and toSNorm could probably go in Math.js.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be happy to. I was on the fence about moving these into Core in this pull request since it is currently only used in this location.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least the geometry pipeline will also use them. Probably dynamic buffers too if it is a win (I bet yes) ( #932). I could also see billboards using something similar for vertex compression ( #419). 3D models will also use this (but won't need the code since it will be done server-side).

out.x = vector.x / (Math.abs(vector.x) + Math.abs(vector.y) + Math.abs(vector.z));
out.y = vector.y / (Math.abs(vector.x) + Math.abs(vector.y) + Math.abs(vector.z));
if (vector.z < 0) {
var x = out.x;
var y = out.y;
out.x = (1.0 - Math.abs(y)) * signNotZero(x);
out.y = (1.0 - Math.abs(x)) * signNotZero(y);
}

out.x = toSNorm(out.x);
out.y = toSNorm(out.y);
}

var encodedScratch = new Cartesian2();
// An upsampled triangle may be clipped twice before it is assigned an index
// In this case, we need a buffer to handle the recursion of getNormalX() and getNormalY().
var depth = -1;
var cartesianScratch1 = [new Cartesian3(), new Cartesian3()];
var cartesianScratch2 = [new Cartesian3(), new Cartesian3()];
function lerpOctEncodedNormal(vertex, result) {
depth += 1;

var first = cartesianScratch1[depth];
var second = cartesianScratch2[depth];

first = octDecode(vertex.first.getNormalX(), vertex.first.getNormalY(), first);
second = octDecode(vertex.second.getNormalX(), vertex.second.getNormalY(), second);
cartesian3Scratch = Cartesian3.lerp(first, second, vertex.ratio, cartesian3Scratch);

octEncode(cartesian3Scratch, result);

depth -= 1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicky, but why not --depth (and similar above)?


return result;
}

Vertex.prototype.getNormalX = function() {
if (defined(this.index)) {
return this.normalBuffer[this.index * 2];
}
return Math.round(CesiumMath.lerp(this.first.getNormalX(), this.second.getNormalX(), this.ratio));

encodedScratch = lerpOctEncodedNormal(this, encodedScratch);
return encodedScratch.x;
};

Vertex.prototype.getNormalY = function() {
if (defined(this.index)) {
return this.normalBuffer[this.index * 2 + 1];
}
return Math.round(CesiumMath.lerp(this.first.getNormalY(), this.second.getNormalY(), this.ratio));

encodedScratch = lerpOctEncodedNormal(this, encodedScratch);
return encodedScratch.y;
};

var polygonVertices = [];
Expand Down
76 changes: 76 additions & 0 deletions Specs/Core/QuantizedMeshTerrainDataSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,82 @@ defineSuite([
});
});

it('oct-encoded normals works for all four children of a simple quad', function() {
var data = new QuantizedMeshTerrainData({
minimumHeight : 0.0,
maximumHeight : 4.0,
quantizedVertices : new Uint16Array([ // order is sw nw se ne
// u
0, 0, 32767, 32767,
// v
0, 32767, 0, 32767,
// heights
32767 / 4.0, 2.0 * 32767 / 4.0, 3.0 * 32767 / 4.0, 32767
]),
encodedNormals : new Uint8Array([
// fun property of oct-encoded normals: the octrahedron is projected onto a plane
// and unfolded into a unit square. The 4 corners of this unit square are encoded values
// of the same Cartesian normal, vec3(0.0, 0.0, 1.0).
// Therefore, all 4 normals below are actually oct-encoded representations of vec3(0.0, 0.0, 1.0)
255, 0, // sw
255, 255, // nw
255, 0, // se
255, 255 // ne
]),
indices : new Uint16Array([
0, 3, 1,
0, 2, 3
]),
boundingSphere : new BoundingSphere(),
horizonOcclusionPoint : new Cartesian3(),
westIndices : [],
southIndices : [],
eastIndices : [],
northIndices : [],
westSkirtHeight : 1.0,
southSkirtHeight : 1.0,
eastSkirtHeight : 1.0,
northSkirtHeight : 1.0,
childTileMask : 15
});

var tilingScheme = new GeographicTilingScheme();

var swPromise = data.upsample(tilingScheme, 0, 0, 0, 0, 0, 1);
var sePromise = data.upsample(tilingScheme, 0, 0, 0, 1, 0, 1);
var nwPromise = data.upsample(tilingScheme, 0, 0, 0, 0, 1, 1);
var nePromise = data.upsample(tilingScheme, 0, 0, 0, 1, 1, 1);

var upsampleResults;

when.all([swPromise, sePromise, nwPromise, nePromise], function(results) {
upsampleResults = results;
});

waitsFor(function() {
return defined(upsampleResults);
});

runs(function() {
expect(upsampleResults.length).toBe(4);

for (var i = 0; i < upsampleResults.length; ++i) {
var upsampled = upsampleResults[i];
expect(upsampled).toBeDefined();

var encodedNormals = upsampled._encodedNormals;

expect(encodedNormals.length).toBe(8);

// All 4 normals should remain oct-encoded representations of vec3(0.0, 0.0, 1.0)
for (var n = 0; n < encodedNormals.length; ++n) {
expect(encodedNormals[i]).toBe(255);
}
}
});

});

it('works for a quad with an extra vertex in the northwest child', function() {
var data = new QuantizedMeshTerrainData({
minimumHeight : 0.0,
Expand Down