Time for another Frankenstein post. This time we are going to combine the following:

The end result is going to be a Cubic Hermite Rectangle Surface like the below. Note that the curve only passes through the inner four control points, and the outer ring of 12 control points are used to determine the slope.

Just like the cubic hermite curve counterpart, a cubic hermite rectangle surface is C1 continuous everywhere, which is great for use as a way of modeling geometry, as well as just for interpolation of multidimensional data. In the image below, each checkerboard square is an individual hermite rectangle.

The links section at the bottom has links to the shadertoys I made that I got the screenshots from.

## Code

Here’s some C++ code that does bicubic hermite interpolation

#include <stdio.h> #include <array> typedef std::array<float, 4> TFloat4; typedef std::array<TFloat4, 4> TFloat4x4; const TFloat4x4 c_ControlPointsX = { { { 0.7f, 0.8f, 0.9f, 0.3f }, { 0.2f, 0.5f, 0.4f, 0.1f }, { 0.6f, 0.3f, 0.1f, 0.4f }, { 0.8f, 0.4f, 0.2f, 0.7f }, } }; const TFloat4x4 c_ControlPointsY = { { { 0.2f, 0.8f, 0.5f, 0.6f }, { 0.6f, 0.9f, 0.3f, 0.8f }, { 0.7f, 0.1f, 0.4f, 0.9f }, { 0.6f, 0.5f, 0.3f, 0.2f }, } }; const TFloat4x4 c_ControlPointsZ = { { { 0.6f, 0.5f, 0.3f, 0.2f }, { 0.7f, 0.1f, 0.9f, 0.5f }, { 0.8f, 0.4f, 0.2f, 0.7f }, { 0.6f, 0.3f, 0.1f, 0.4f }, } }; void WaitForEnter () { printf("Press Enter to quit"); fflush(stdin); getchar(); } // t is a value that goes from 0 to 1 to interpolate in a C1 continuous way across uniformly sampled data points. // when t is 0, this will return p[1]. When t is 1, this will return p[2]. // p[0] and p[3] are used to calculate slopes at the edges. float CubicHermite(const TFloat4& p, float t) { float a = -p[0] / 2.0f + (3.0f*p[1]) / 2.0f - (3.0f*p[2]) / 2.0f + p[3] / 2.0f; float b = p[0] - (5.0f*p[1]) / 2.0f + 2.0f*p[2] - p[3] / 2.0f; float c = -p[0] / 2.0f + p[2] / 2.0f; float d = p[1]; return a*t*t*t + b*t*t + c*t + d; } float BicubicHermitePatch(const TFloat4x4& p, float u, float v) { TFloat4 uValues; uValues[0] = CubicHermite(p[0], u); uValues[1] = CubicHermite(p[1], u); uValues[2] = CubicHermite(p[2], u); uValues[3] = CubicHermite(p[2], u); return CubicHermite(uValues, v); } int main(int argc, char **argv) { // how many values to display on each axis. Limited by console resolution! const int c_numValues = 4; printf("Cubic Hermite rectangle:\n"); for (int i = 0; i < c_numValues; ++i) { float iPercent = ((float)i) / ((float)(c_numValues - 1)); for (int j = 0; j < c_numValues; ++j) { if (j == 0) printf(" "); float jPercent = ((float)j) / ((float)(c_numValues - 1)); float valueX = BicubicHermitePatch(c_ControlPointsX, jPercent, iPercent); float valueY = BicubicHermitePatch(c_ControlPointsY, jPercent, iPercent); float valueZ = BicubicHermitePatch(c_ControlPointsZ, jPercent, iPercent); printf("(%0.2f, %0.2f, %0.2f) ", valueX, valueY, valueZ); } printf("\n"); } printf("\n"); WaitForEnter(); return 0; }

And here’s the output. Note that the four corners of the output correspond to the four inner most points defined in the data!

## On The GPU / Links

While cubic Hermite rectangles pass through all of their control points like Lagrange surfaces do (and like Bezier rectangle’s don’t), they don’t suffer from Runge’s Phenomenon like Lagrange surfaces do.

However, just like Lagrange surfaces, Hermite surfaces don’t have the nice property that Bezier surfaces have, where the surface is guaranteed to stay inside of the convex hull defined by the control points.

Since Hermite surfaces are just cubic functions though, you could calculate the minimum and maximum value that they can reach using some calculus and come up with a bounding box by going that direction. The same thing is technically true of Lagrange surfaces as well for what it’s worth.

Check out the links below to see cubic Hermite rectangles rendered in real time in WebGL using raytracing and raymarching:

Shadertoy: Cubic Hermite Rectangle

Shadertoy: Infinite Hermite Rectangles