Vector transformation using projection between orthonormal bases

$a_{\text{object}}$ is a vector in the object space, which has an orthonormal basis $(u, v, w)$.

$$ a_{\text{object}}(u, v, w) = \text{u} u + \text{v} v + \text{w} w. $$

The projection relation between $(u, v, w)$ and the world space orthonormal basis $(x, y, z)$ is:

$$ u(x, y, z) = \text{x}_u x + \text{y}_u y + \text{z}_u z, $$ $$ v(x, y, z) = \text{x}_v x + \text{y}_v y + \text{z}_v z, $$ $$ v(x, y, z) = \text{x}_w x + \text{y}_w y + \text{z}_w z. $$

$a_{\text{world}}$ can be solved using the above equations.

The following code describes the process of cosine-weighted sampling on a hemisphere with a random zenith direction. Firstly, a sample is produced in the object(tangent) space where the zenith direction is always pointing upwards. The sample is then transformed to the world space uisng orthonormal basis projection.

# orthonormal basis order
t_idx=0 # tangent (left)
bn_idx=1 # binormal (front)
n_idx=2 # normal (zenith/up)

def get_onb(n):
    binormal = []
    tangent = []
    t = [n[0], n[1], n[2] + 1]
    binormal = normalize(cross(n, t))
    tangent = cross(n, binormal)
    return [tangent,  binormal, n]

def onb_inverse_transform(onb, p):
    # transform point p from tangent space to world space
    return add(scale(onb[t_idx], p[t_idx]), scale(onb[bn_idx], p[bn_idx]), scale(onb[n_idx], p[n_idx]))

# generate a random surface normal to construct the tangent space
nff = [- 1.0 + 2.0 * rnd(), - 1.0 + 2.0 * rnd(), rnd()] 
nff = normalize(nff)
onb = get_onb(nff)

for i_phi in range(0, num_sample_phi):
    for i_theta in range(0, num_sample_theta):
       phi = map_ep_to_phi(rnd())
       theta_diffuse = map_ep_to_theta_diffuse(rnd())

       # tangent space sample
       x = spherical_to_x(phi, theta_diffuse)
       y = spherical_to_y(phi, theta_diffuse)
       z = spherical_to_z(phi, theta_diffuse)
       data_x_object.append(x)
       data_y_object.append(y)
       data_z_object.append(z)

       # world space sample
       p = [x, y, z]
       p = onb_inverse_transform(onb, p)
       data_x_world.append(p[0])
       data_y_world.append(p[1])
       data_z_world.append(p[2])

Result (Left: tangent space. Right: world space. The blue dot indicates where the random surface normal intersects the unit sphere.)

Imgur