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
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ Change Log
* Added northUpEast transform to help support display of glTF models because Y is their up axis.
* Cesium can now render an unlimited number of imagery layers, no matter how few texture units are supported by the hardware.
* Added `czm_octDecode` and `czm_signNotZero` builtin functions.
* Added `Oct` namespace that defines static functions for encoding and decoding normalized unit vectors to and from oct-encoding.
Copy link
Member

Choose a reason for hiding this comment

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

This needs to go away now that Oct is private.

* Added `CesiumMath.signNotZero`, `CesiumMath.toSNorm` and `CesiumMath.fromSNorm` functions in support of oct-encoding.
* Added `CesiumTerrainProvider.requestVertexNormals` to request per vertex normals from the provider, if they are available.
* Added new property to all terrain providers: `TerrainProvider.hasVertexNormals`, `CesiumTerrainProvider.hasVertexNormals`, `ArcGisImageServerTerrainProvider.hasVertexNormals`, `EllipsoidTerrainProvider.hasVertexNormals`, `VRTheWorldTerrainProvider.hasVertexNormals`. This property indicates whether or not vertex normals will be included in the terrain tile responses.
* Added support for rendering the globe with oct-encoded per vertex normals .
Expand Down
33 changes: 33 additions & 0 deletions Source/Core/Math.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,39 @@ define([
return 0;
};

/**
* Returns 1.0 if the given value is positive or zero, and -1.0 if it is negative.
* This is similar to {@link CesiumMath#sign} except that returns 1.0 instead of
* 0.0 when the input value is 0.0.
* @param {Number} value The value to return the sign of.
* @returns {Number} The sign of value.
*/
CesiumMath.signNotZero = function(value) {
return value < 0.0 ? -1.0 : 1.0;
};

/**
* Converts a scalar value in the range [-1.0, 1.0] to a 8-bit 2's complement number.
* @param {Number} value The scalar value in the range [-1.0, 1.0]
* @returns {Number} The 8-bit 2's complement number, where 0 maps to -1.0 and 255 maps to 1.0.
*
* @see CesiumMath.fromSNorm
*/
CesiumMath.toSNorm = function(value) {
return Math.round((CesiumMath.clamp(value, -1.0, 1.0) * 0.5 + 0.5) * 255.0);
};

/**
* Converts a SNORM value in the range [0, 255] to a scalar in the range [-1.0, 1.0].
* @param {Number} value SNORM value in the range [0, 255]
* @returns {Number} Scalar in the range [-1.0, 1.0].
*
* @see CesiumMath.toSNorm
*/
CesiumMath.fromSNorm = function(value) {
return CesiumMath.clamp(value, 0.0, 255.0) / 255.0 * 2.0 - 1.0;
};

/**
* Returns the hyperbolic sine of a number.
* The hyperbolic sine of <em>value</em> is defined to be
Expand Down
102 changes: 102 additions & 0 deletions Source/Core/Oct.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*global define*/
define([
'./Cartesian3',
'./Math',
'./defined',
'./DeveloperError'
], function(
Cartesian3,
CesiumMath,
defined,
DeveloperError) {
"use strict";

/**
* Oct encoding and decoding functions.
*
* Oct encoding is a compact representation of unit length vectors. The encoding and decoding functions are low cost, and represent the normalized vector within 1 degree of error.
* The 'oct' encoding is described in "A Survey of Efficient Representations of Independent Unit Vectors",
* Cigolle et al 2014: {@link http://jcgt.org/published/0003/02/01/}
*
* @namespace
* @alias Oct
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this need to be public? Seems like it would be a private class to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, seems like it probably should be private. Easy enough to make it public in the future if there's a need for it.

*/
var Oct = {};

/**
* Encodes a normalized vector into 2 SNORM values in the range of [0-255] following the 'oct' encoding.
*
* @param {Cartesian3} vector The normalized vector to be compressed into 2 byte 'oct' encoding.
* @param {Cartesian2} result The 2 byte oct-encoded unit length vector.
* @returns {Cartesian2} The 2 byte oct-encoded unit length vector.
*
* @exception {DeveloperError} vector must be defined.
* @exception {DeveloperError} result must be defined.
* @exception {DeveloperError} vector must be normalized.
*/
Oct.encode = function(vector, result) {
//>>includeStart('debug', pragmas.debug);
if (!defined(vector)) {
throw new DeveloperError('vector is required.');
}
if (!defined(result)) {
throw new DeveloperError('result is required.');
}
var magSquared = Cartesian3.magnitudeSquared(vector);
if (Math.abs(magSquared - 1.0) > CesiumMath.EPSILON6) {
throw new DeveloperError('vector must be normalized.');
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be documented.

}
//>>includeEnd('debug');

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

result.x = CesiumMath.toSNorm(result.x);
result.y = CesiumMath.toSNorm(result.y);

return result;
};

/**
* Decodes a unit-length vector in 'oct' encoding to a normalized 3-component vector.
*
* @param {Number} x The x component of the oct-encoded unit length vector.
* @param {Number} y The y component of the oct-encoded unit length vector.
* @param {Cartesian3} result The decoded and normalized vector
* @returns {Cartesian3} The decoded and normalized vector.
*
* @exception {DeveloperError} result must be defined.
* @exception {DeveloperError} x and y must be a signed normalized integer between 0 and 255.
*/
Oct.decode = function(x, y, result) {
//>>includeStart('debug', pragmas.debug);
if (!defined(result)) {
throw new DeveloperError('result is required.');
}
if (x < 0 || x > 255 || y < 0 || y > 255) {
throw new DeveloperError('x and y must be a signed normalized integer between 0 and 255');
}
//>>includeEnd('debug');

result.x = CesiumMath.fromSNorm(x);
result.y = CesiumMath.fromSNorm(y);
result.z = 1.0 - (Math.abs(result.x) + Math.abs(result.y));

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

return Cartesian3.normalize(result, result);
};

return Oct;
});
36 changes: 34 additions & 2 deletions Source/Workers/upsampleQuantizedTerrainMesh.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
/*global define*/
define([
'../Core/BoundingSphere',
'../Core/Cartesian2',
'../Core/Cartesian3',
'../Core/Cartographic',
'../Core/defined',
'../Core/Ellipsoid',
'../Core/EllipsoidalOccluder',
'../Core/Intersections2D',
'../Core/Math',
'../Core/Oct',
'./createTaskProcessorWorker'
], function(
BoundingSphere,
Cartesian2,
Cartesian3,
Cartographic,
defined,
Ellipsoid,
EllipsoidalOccluder,
Intersections2D,
CesiumMath,
Oct,
createTaskProcessorWorker) {
"use strict";

Expand Down Expand Up @@ -372,18 +376,46 @@ define([
return CesiumMath.lerp(this.first.getV(), this.second.getV(), this.ratio);
};

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 = Oct.decode(vertex.first.getNormalX(), vertex.first.getNormalY(), first);
second = Oct.decode(vertex.second.getNormalX(), vertex.second.getNormalY(), second);
cartesian3Scratch = Cartesian3.lerp(first, second, vertex.ratio, cartesian3Scratch);
Cartesian3.normalize(cartesian3Scratch, cartesian3Scratch);

Oct.encode(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
53 changes: 53 additions & 0 deletions Specs/Core/MathSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,59 @@ defineSuite([
expect(CesiumMath.sign(0)).toEqual(0);
});

it('signNotZero of -2', function() {
expect(CesiumMath.signNotZero(-2)).toEqual(-1);
});

it('signNotZero of 2', function() {
expect(CesiumMath.signNotZero(2)).toEqual(1);
});

it('signNotZero of 0', function() {
expect(CesiumMath.signNotZero(0)).toEqual(1);
});

//////////////////////////////////////////////////////////////////////
it('toSNorm -1.0', function() {
expect(CesiumMath.toSNorm(-1.0)).toEqual(0);
});

it('toSNorm 1.0', function() {
expect(CesiumMath.toSNorm(1.0)).toEqual(255);
});

it('toSNorm -1.0001', function() {
expect(CesiumMath.toSNorm(-1.0001)).toEqual(0);
});

it('toSNorm 1.0001', function() {
expect(CesiumMath.toSNorm(1.0001)).toEqual(255);
});

it('toSNorm 0.0', function() {
expect(CesiumMath.toSNorm(0.0)).toEqual(128);
});

it('fromSNorm 0', function() {
expect(CesiumMath.fromSNorm(0)).toEqual(-1.0);
});

it('fromSNorm 255', function() {
expect(CesiumMath.fromSNorm(255)).toEqual(1.0);
});

it('fromSNorm -0.0001', function() {
expect(CesiumMath.fromSNorm(-0.0001)).toEqual(-1.0);
});

it('fromSNorm 255.00001', function() {
expect(CesiumMath.fromSNorm(255.00001)).toEqual(1.0);
});

it('fromSNorm 128', function() {
expect(CesiumMath.fromSNorm(255.0 / 2)).toEqual(0.0);
});

//////////////////////////////////////////////////////////////////////

it('cosh', function() {
Expand Down
101 changes: 101 additions & 0 deletions Specs/Core/OctSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*global defineSuite*/
defineSuite([
'Core/Oct',
'Core/Cartesian2',
'Core/Cartesian3'
], function(
Oct,
Cartesian2,
Cartesian3) {
"use strict";
/*global jasmine,describe,xdescribe,it,xit,expect,beforeEach,afterEach,beforeAll,afterAll,spyOn,runs,waits,waitsFor*/

var negativeUnitZ = new Cartesian3(0.0, 0.0, -1.0);
it('oct decode(0, 0)', function() {
var result = new Cartesian3();
Oct.decode(0, 0, result);
expect(result).toEqual(negativeUnitZ);
});

it('oct encode(0, 0, -1)', function() {
var result = new Cartesian2();
Oct.encode(negativeUnitZ, result);
expect(result).toEqual(new Cartesian2(255, 255));
});

it('oct encode(0, 0, 1)', function() {
var result = new Cartesian2();
Oct.encode(Cartesian3.UNIT_Z, result);
expect(result).toEqual(new Cartesian2(128, 128));
});

it('oct extents are equal', function() {
var result = new Cartesian3();
// lower left
Oct.decode(0, 0, result);
expect(result).toEqual(negativeUnitZ);
// lower right
Oct.decode(255, 0, result);
expect(result).toEqual(negativeUnitZ);
// upper right
Oct.decode(255, 255, result);
expect(result).toEqual(negativeUnitZ);
// upper left
Oct.decode(255, 0, result);
expect(result).toEqual(negativeUnitZ);
});

it('throws oct encode vector undefined', function() {
var vector;
var result = new Cartesian3();
expect(function() {
Oct.encode(vector, result);
}).toThrowDeveloperError();
});

it('throws oct encode result undefined', function() {
var result;
expect(function() {
Oct.encode(Cartesian3.UNIT_Z, result);
}).toThrowDeveloperError();
});

it('throws oct encode non unit vector', function() {
var nonUnitLengthVector = new Cartesian3(2.0, 0.0, 0.0);
var result = new Cartesian2();
expect(function() {
Oct.encode(nonUnitLengthVector, result);
}).toThrowDeveloperError();
});

it('throws oct encode zero length vector', function() {
var result = new Cartesian2();
expect(function() {
Oct.encode(Cartesian3.ZERO, result);
}).toThrowDeveloperError();
});

it('throws oct decode result undefined', function() {
var result;
expect(function() {
Oct.decode(Cartesian2.ZERO, result);
}).toThrowDeveloperError();
});

it('throws oct decode x out of bounds', function() {
var result = new Cartesian3();
var invalidSNorm = new Cartesian2(256, 0);
expect(function() {
Oct.decode(invalidSNorm, result);
}).toThrowDeveloperError();
});

it('throws oct decode y out of bounds', function() {
var result = new Cartesian3();
var invalidSNorm = new Cartesian2(0, 256);
expect(function() {
Oct.decode(invalidSNorm, result);
}).toThrowDeveloperError();
});

});
Loading