Cycles ignores the size of spot lights, therefore the illuminated area doesn't match the gizmo. This patch resolves this discrepancy.
| Before (Cycles) | After (Cycles) | Eevee
|{F14200605}|{F14200595}|{F14200600}|
This is done by scaling the ray direction by the size of the cone. The implementation of `spot_light_attenuation()` in `spot.h` matches `spot_attenuation()` in `lights_lib.glsl`.
**Test file**:
{F14200728}
Differential Revision: https://developer.blender.org/D17129
402 lines
14 KiB
C++
402 lines
14 KiB
C++
/* SPDX-License-Identifier: Apache-2.0
|
|
* Copyright 2011-2022 Blender Foundation */
|
|
|
|
#include "scene/light_tree.h"
|
|
#include "scene/mesh.h"
|
|
#include "scene/object.h"
|
|
|
|
CCL_NAMESPACE_BEGIN
|
|
|
|
float OrientationBounds::calculate_measure() const
|
|
{
|
|
float theta_w = fminf(M_PI_F, theta_o + theta_e);
|
|
float cos_theta_o = cosf(theta_o);
|
|
float sin_theta_o = sinf(theta_o);
|
|
|
|
return M_2PI_F * (1 - cos_theta_o) +
|
|
M_PI_2_F * (2 * theta_w * sin_theta_o - cosf(theta_o - 2 * theta_w) -
|
|
2 * theta_o * sin_theta_o + cos_theta_o);
|
|
}
|
|
|
|
OrientationBounds merge(const OrientationBounds &cone_a, const OrientationBounds &cone_b)
|
|
{
|
|
if (is_zero(cone_a.axis)) {
|
|
return cone_b;
|
|
}
|
|
if (is_zero(cone_b.axis)) {
|
|
return cone_a;
|
|
}
|
|
|
|
/* Set cone a to always have the greater theta_o. */
|
|
const OrientationBounds *a = &cone_a;
|
|
const OrientationBounds *b = &cone_b;
|
|
if (cone_b.theta_o > cone_a.theta_o) {
|
|
a = &cone_b;
|
|
b = &cone_a;
|
|
}
|
|
|
|
float theta_d = safe_acosf(dot(a->axis, b->axis));
|
|
float theta_e = fmaxf(a->theta_e, b->theta_e);
|
|
|
|
/* Return axis and theta_o of a if it already contains b. */
|
|
/* This should also be called when b is empty. */
|
|
if (a->theta_o >= fminf(M_PI_F, theta_d + b->theta_o)) {
|
|
return OrientationBounds({a->axis, a->theta_o, theta_e});
|
|
}
|
|
|
|
/* Compute new theta_o that contains both a and b. */
|
|
float theta_o = (theta_d + a->theta_o + b->theta_o) * 0.5f;
|
|
|
|
if (theta_o >= M_PI_F) {
|
|
return OrientationBounds({a->axis, M_PI_F, theta_e});
|
|
}
|
|
|
|
/* Rotate new axis to be between a and b. */
|
|
float theta_r = theta_o - a->theta_o;
|
|
float3 new_axis = rotate_around_axis(a->axis, cross(a->axis, b->axis), theta_r);
|
|
new_axis = normalize(new_axis);
|
|
|
|
return OrientationBounds({new_axis, theta_o, theta_e});
|
|
}
|
|
|
|
LightTreePrimitive::LightTreePrimitive(Scene *scene, int prim_id, int object_id)
|
|
: prim_id(prim_id), object_id(object_id)
|
|
{
|
|
bcone = OrientationBounds::empty;
|
|
bbox = BoundBox::empty;
|
|
|
|
if (is_triangle()) {
|
|
float3 vertices[3];
|
|
Object *object = scene->objects[object_id];
|
|
Mesh *mesh = static_cast<Mesh *>(object->get_geometry());
|
|
Mesh::Triangle triangle = mesh->get_triangle(prim_id);
|
|
Shader *shader = static_cast<Shader *>(mesh->get_used_shaders()[mesh->get_shader()[prim_id]]);
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
vertices[i] = mesh->get_verts()[triangle.v[i]];
|
|
}
|
|
|
|
/* instanced mesh lights have not applied their transform at this point.
|
|
* in this case, these points have to be transformed to get the proper
|
|
* spatial bound. */
|
|
if (!mesh->transform_applied) {
|
|
const Transform &tfm = object->get_tfm();
|
|
for (int i = 0; i < 3; i++) {
|
|
vertices[i] = transform_point(&tfm, vertices[i]);
|
|
}
|
|
}
|
|
|
|
/* TODO: need a better way to handle this when textures are used. */
|
|
float area = triangle_area(vertices[0], vertices[1], vertices[2]);
|
|
energy = area * average(shader->emission_estimate);
|
|
|
|
/* NOTE: the original implementation used the bounding box centroid, but primitive centroid
|
|
* seems to work fine */
|
|
centroid = (vertices[0] + vertices[1] + vertices[2]) / 3.0f;
|
|
|
|
const bool is_front_only = (shader->emission_sampling == EMISSION_SAMPLING_FRONT);
|
|
const bool is_back_only = (shader->emission_sampling == EMISSION_SAMPLING_BACK);
|
|
if (is_front_only || is_back_only) {
|
|
/* One-sided. */
|
|
bcone.axis = safe_normalize(cross(vertices[1] - vertices[0], vertices[2] - vertices[0]));
|
|
if (is_back_only) {
|
|
bcone.axis = -bcone.axis;
|
|
}
|
|
if (transform_negative_scale(object->get_tfm())) {
|
|
bcone.axis = -bcone.axis;
|
|
}
|
|
bcone.theta_o = 0;
|
|
}
|
|
else {
|
|
/* Double sided: any vector in the plane. */
|
|
bcone.axis = safe_normalize(vertices[0] - vertices[1]);
|
|
bcone.theta_o = M_PI_2_F;
|
|
}
|
|
bcone.theta_e = M_PI_2_F;
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
bbox.grow(vertices[i]);
|
|
}
|
|
}
|
|
else {
|
|
Light *lamp = scene->lights[object_id];
|
|
LightType type = lamp->get_light_type();
|
|
const float size = lamp->get_size();
|
|
float3 strength = lamp->get_strength();
|
|
|
|
centroid = scene->lights[object_id]->get_co();
|
|
bcone.axis = normalize(lamp->get_dir());
|
|
|
|
if (type == LIGHT_AREA) {
|
|
bcone.theta_o = 0;
|
|
bcone.theta_e = lamp->get_spread() * 0.5f;
|
|
|
|
/* For an area light, sizeu and sizev determine the 2 dimensions of the area light,
|
|
* while axisu and axisv determine the orientation of the 2 dimensions.
|
|
* We want to add all 4 corners to our bounding box. */
|
|
const float3 half_extentu = 0.5f * lamp->get_sizeu() * lamp->get_axisu() * size;
|
|
const float3 half_extentv = 0.5f * lamp->get_sizev() * lamp->get_axisv() * size;
|
|
bbox.grow(centroid + half_extentu + half_extentv);
|
|
bbox.grow(centroid + half_extentu - half_extentv);
|
|
bbox.grow(centroid - half_extentu + half_extentv);
|
|
bbox.grow(centroid - half_extentu - half_extentv);
|
|
|
|
strength *= 0.25f; /* eval_fac scaling in `area.h` */
|
|
}
|
|
else if (type == LIGHT_POINT) {
|
|
bcone.theta_o = M_PI_F;
|
|
bcone.theta_e = M_PI_2_F;
|
|
|
|
/* Point and spot lights can emit light from any point within its radius. */
|
|
const float3 radius = make_float3(size);
|
|
bbox.grow(centroid - radius);
|
|
bbox.grow(centroid + radius);
|
|
|
|
strength *= 0.25f * M_1_PI_F; /* eval_fac scaling in `spot.h` and `point.h` */
|
|
}
|
|
else if (type == LIGHT_SPOT) {
|
|
bcone.theta_o = 0;
|
|
|
|
const float unscaled_theta_e = lamp->get_spot_angle() * 0.5f;
|
|
const float len_u = len(lamp->get_axisu());
|
|
const float len_v = len(lamp->get_axisv());
|
|
const float len_w = len(lamp->get_dir());
|
|
|
|
bcone.theta_e = fast_atanf(fast_tanf(unscaled_theta_e) * fmaxf(len_u, len_v) / len_w);
|
|
|
|
/* Point and spot lights can emit light from any point within its radius. */
|
|
const float3 radius = make_float3(size);
|
|
bbox.grow(centroid - radius);
|
|
bbox.grow(centroid + radius);
|
|
|
|
strength *= 0.25f * M_1_PI_F; /* eval_fac scaling in `spot.h` and `point.h` */
|
|
}
|
|
else if (type == LIGHT_BACKGROUND) {
|
|
/* Set an arbitrary direction for the background light. */
|
|
bcone.axis = make_float3(0.0f, 0.0f, 1.0f);
|
|
/* TODO: this may depend on portal lights as well. */
|
|
bcone.theta_o = M_PI_F;
|
|
bcone.theta_e = 0;
|
|
|
|
/* integrate over cosine-weighted hemisphere */
|
|
strength *= lamp->get_average_radiance() * M_PI_F;
|
|
}
|
|
else if (type == LIGHT_DISTANT) {
|
|
bcone.theta_o = 0;
|
|
bcone.theta_e = 0.5f * lamp->get_angle();
|
|
}
|
|
|
|
if (lamp->get_shader()) {
|
|
strength *= lamp->get_shader()->emission_estimate;
|
|
}
|
|
|
|
/* Use absolute value of energy so lights with negative strength are properly
|
|
* supported in the light tree. */
|
|
energy = fabsf(average(strength));
|
|
}
|
|
}
|
|
|
|
LightTree::LightTree(vector<LightTreePrimitive> &prims,
|
|
const int &num_distant_lights,
|
|
uint max_lights_in_leaf)
|
|
{
|
|
if (prims.empty()) {
|
|
return;
|
|
}
|
|
|
|
max_lights_in_leaf_ = max_lights_in_leaf;
|
|
int num_prims = prims.size();
|
|
int num_local_lights = num_prims - num_distant_lights;
|
|
/* The amount of nodes is estimated to be twice the amount of primitives */
|
|
nodes_.reserve(2 * num_prims);
|
|
|
|
nodes_.emplace_back(); /* root node */
|
|
recursive_build(0, num_local_lights, prims, 0, 1); /* build tree */
|
|
nodes_[0].make_interior(nodes_.size());
|
|
|
|
/* All distant lights are grouped to one node (right child of the root node) */
|
|
OrientationBounds bcone = OrientationBounds::empty;
|
|
float energy_total = 0.0;
|
|
for (int i = num_local_lights; i < num_prims; i++) {
|
|
const LightTreePrimitive &prim = prims.at(i);
|
|
bcone = merge(bcone, prim.bcone);
|
|
energy_total += prim.energy;
|
|
}
|
|
nodes_.emplace_back(BoundBox::empty, bcone, energy_total, 1);
|
|
nodes_.back().make_leaf(num_local_lights, num_distant_lights);
|
|
|
|
nodes_.shrink_to_fit();
|
|
}
|
|
|
|
const vector<LightTreeNode> &LightTree::get_nodes() const
|
|
{
|
|
return nodes_;
|
|
}
|
|
|
|
int LightTree::recursive_build(
|
|
int start, int end, vector<LightTreePrimitive> &prims, uint bit_trail, int depth)
|
|
{
|
|
BoundBox bbox = BoundBox::empty;
|
|
OrientationBounds bcone = OrientationBounds::empty;
|
|
BoundBox centroid_bounds = BoundBox::empty;
|
|
float energy_total = 0.0;
|
|
int num_prims = end - start;
|
|
int current_index = nodes_.size();
|
|
|
|
for (int i = start; i < end; i++) {
|
|
const LightTreePrimitive &prim = prims.at(i);
|
|
bbox.grow(prim.bbox);
|
|
bcone = merge(bcone, prim.bcone);
|
|
centroid_bounds.grow(prim.centroid);
|
|
|
|
energy_total += prim.energy;
|
|
}
|
|
|
|
nodes_.emplace_back(bbox, bcone, energy_total, bit_trail);
|
|
|
|
bool try_splitting = num_prims > 1 && len(centroid_bounds.size()) > 0.0f;
|
|
int split_dim = -1, split_bucket = 0, num_left_prims = 0;
|
|
bool should_split = false;
|
|
if (try_splitting) {
|
|
/* Find the best place to split the primitives into 2 nodes.
|
|
* If the best split cost is no better than making a leaf node, make a leaf instead.*/
|
|
float min_cost = min_split_saoh(
|
|
centroid_bounds, start, end, bbox, bcone, split_dim, split_bucket, num_left_prims, prims);
|
|
should_split = num_prims > max_lights_in_leaf_ || min_cost < energy_total;
|
|
}
|
|
if (should_split) {
|
|
int middle;
|
|
|
|
if (split_dim != -1) {
|
|
/* Partition the primitives between start and end based on the split dimension and bucket
|
|
* calculated by `split_saoh` */
|
|
middle = start + num_left_prims;
|
|
std::nth_element(prims.begin() + start,
|
|
prims.begin() + middle,
|
|
prims.begin() + end,
|
|
[split_dim](const LightTreePrimitive &l, const LightTreePrimitive &r) {
|
|
return l.centroid[split_dim] < r.centroid[split_dim];
|
|
});
|
|
}
|
|
else {
|
|
/* Degenerate case with many lights in the same place. */
|
|
middle = (start + end) / 2;
|
|
}
|
|
|
|
[[maybe_unused]] int left_index = recursive_build(start, middle, prims, bit_trail, depth + 1);
|
|
int right_index = recursive_build(middle, end, prims, bit_trail | (1u << depth), depth + 1);
|
|
assert(left_index == current_index + 1);
|
|
nodes_[current_index].make_interior(right_index);
|
|
}
|
|
else {
|
|
nodes_[current_index].make_leaf(start, num_prims);
|
|
}
|
|
return current_index;
|
|
}
|
|
|
|
float LightTree::min_split_saoh(const BoundBox ¢roid_bbox,
|
|
int start,
|
|
int end,
|
|
const BoundBox &bbox,
|
|
const OrientationBounds &bcone,
|
|
int &split_dim,
|
|
int &split_bucket,
|
|
int &num_left_prims,
|
|
const vector<LightTreePrimitive> &prims)
|
|
{
|
|
/* Even though this factor is used for every bucket, we use it to compare
|
|
* the min_cost and total_energy (when deciding between creating a leaf or interior node. */
|
|
const float bbox_area = bbox.area();
|
|
const bool has_area = bbox_area != 0.0f;
|
|
const float total_area = has_area ? bbox_area : len(bbox.size());
|
|
const float total_cost = total_area * bcone.calculate_measure();
|
|
if (total_cost == 0.0f) {
|
|
return FLT_MAX;
|
|
}
|
|
|
|
const float inv_total_cost = 1.0f / total_cost;
|
|
const float3 extent = centroid_bbox.size();
|
|
const float max_extent = max4(extent.x, extent.y, extent.z, 0.0f);
|
|
|
|
/* Check each dimension to find the minimum splitting cost. */
|
|
float min_cost = FLT_MAX;
|
|
for (int dim = 0; dim < 3; dim++) {
|
|
/* If the centroid bounding box is 0 along a given dimension, skip it. */
|
|
if (centroid_bbox.size()[dim] == 0.0f) {
|
|
continue;
|
|
}
|
|
|
|
const float inv_extent = 1 / (centroid_bbox.size()[dim]);
|
|
|
|
/* Fill in buckets with primitives. */
|
|
vector<LightTreeBucketInfo> buckets(LightTreeBucketInfo::num_buckets);
|
|
for (int i = start; i < end; i++) {
|
|
const LightTreePrimitive &prim = prims[i];
|
|
|
|
/* Place primitive into the appropriate bucket,
|
|
* where the centroid box is split into equal partitions. */
|
|
int bucket_idx = LightTreeBucketInfo::num_buckets *
|
|
(prim.centroid[dim] - centroid_bbox.min[dim]) * inv_extent;
|
|
if (bucket_idx == LightTreeBucketInfo::num_buckets) {
|
|
bucket_idx = LightTreeBucketInfo::num_buckets - 1;
|
|
}
|
|
|
|
buckets[bucket_idx].count++;
|
|
buckets[bucket_idx].energy += prim.energy;
|
|
buckets[bucket_idx].bbox.grow(prim.bbox);
|
|
buckets[bucket_idx].bcone = merge(buckets[bucket_idx].bcone, prim.bcone);
|
|
}
|
|
|
|
/* Calculate the cost of splitting at each point between partitions. */
|
|
vector<float> bucket_costs(LightTreeBucketInfo::num_buckets - 1);
|
|
float energy_L, energy_R;
|
|
BoundBox bbox_L, bbox_R;
|
|
OrientationBounds bcone_L, bcone_R;
|
|
for (int split = 0; split < LightTreeBucketInfo::num_buckets - 1; split++) {
|
|
energy_L = 0;
|
|
energy_R = 0;
|
|
bbox_L = BoundBox::empty;
|
|
bbox_R = BoundBox::empty;
|
|
bcone_L = OrientationBounds::empty;
|
|
bcone_R = OrientationBounds::empty;
|
|
|
|
for (int left = 0; left <= split; left++) {
|
|
if (buckets[left].bbox.valid()) {
|
|
energy_L += buckets[left].energy;
|
|
bbox_L.grow(buckets[left].bbox);
|
|
bcone_L = merge(bcone_L, buckets[left].bcone);
|
|
}
|
|
}
|
|
|
|
for (int right = split + 1; right < LightTreeBucketInfo::num_buckets; right++) {
|
|
if (buckets[right].bbox.valid()) {
|
|
energy_R += buckets[right].energy;
|
|
bbox_R.grow(buckets[right].bbox);
|
|
bcone_R = merge(bcone_R, buckets[right].bcone);
|
|
}
|
|
}
|
|
|
|
/* Calculate the cost of splitting using the heuristic as described in the paper. */
|
|
const float area_L = has_area ? bbox_L.area() : len(bbox_L.size());
|
|
const float area_R = has_area ? bbox_R.area() : len(bbox_R.size());
|
|
float left = (bbox_L.valid()) ? energy_L * area_L * bcone_L.calculate_measure() : 0.0f;
|
|
float right = (bbox_R.valid()) ? energy_R * area_R * bcone_R.calculate_measure() : 0.0f;
|
|
float regularization = max_extent * inv_extent;
|
|
bucket_costs[split] = regularization * (left + right) * inv_total_cost;
|
|
|
|
if (bucket_costs[split] < min_cost) {
|
|
min_cost = bucket_costs[split];
|
|
split_dim = dim;
|
|
split_bucket = split;
|
|
num_left_prims = 0;
|
|
for (int i = 0; i <= split_bucket; i++) {
|
|
num_left_prims += buckets[i].count;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return min_cost;
|
|
}
|
|
|
|
CCL_NAMESPACE_END
|