summaryrefslogtreecommitdiffstats
path: root/src/3rdparty/resonance-audio/resonance_audio/geometrical_acoustics/proxy_room_estimator.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/3rdparty/resonance-audio/resonance_audio/geometrical_acoustics/proxy_room_estimator.cc')
-rw-r--r--src/3rdparty/resonance-audio/resonance_audio/geometrical_acoustics/proxy_room_estimator.cc438
1 files changed, 438 insertions, 0 deletions
diff --git a/src/3rdparty/resonance-audio/resonance_audio/geometrical_acoustics/proxy_room_estimator.cc b/src/3rdparty/resonance-audio/resonance_audio/geometrical_acoustics/proxy_room_estimator.cc
new file mode 100644
index 000000000..d0df0369a
--- /dev/null
+++ b/src/3rdparty/resonance-audio/resonance_audio/geometrical_acoustics/proxy_room_estimator.cc
@@ -0,0 +1,438 @@
+/*
+Copyright 2018 Google Inc. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS-IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+#include "geometrical_acoustics/proxy_room_estimator.h"
+
+#include <algorithm>
+#include <cmath>
+
+#include "Eigen/Core"
+#include "base/logging.h"
+#include "platforms/common/room_effects_utils.h"
+
+namespace vraudio {
+
+namespace {
+
+// Normals of the walls of an axis-aligned cube. Walls are indexed like this:
+// 0: Left (-x)
+// 1: Right (+x)
+// 2: Bottom (-y)
+// 3: Top (+y)
+// 4: Back (-z)
+// 5: Front (+z)
+static const float kCubeWallNormals[kNumRoomSurfaces][3] = {
+ {-1.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, -1.0f, 0.0f},
+ {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, -1.0f}, {0.0f, 0.0f, 1.0f}};
+
+// A helper function to determine if a ray (shooting from |ray_origin| in the
+// |ray_direction| direction) intersects with a plane (whose normal is
+// |plane_normal| and whose distance to the origin is
+// |plane_distance_to_origin|). When returning true, the intersection point is
+// stored in |intersection|.
+bool PlaneIntersection(const WorldPosition& plane_normal,
+ float plane_distance_to_origin,
+ const WorldPosition& ray_origin,
+ const WorldPosition& ray_direction,
+ WorldPosition* intersection) {
+ // Do not count if the direction is too close to parallel to the plane (i.e.,
+ // the dot product of |direction| and |plane_normal|, |d_dot_n| is too close
+ // to zero).
+ const float d_dot_n = ray_direction.dot(plane_normal);
+ if (std::abs(d_dot_n) < vraudio::kEpsilonFloat) {
+ return false;
+ }
+
+ // Find the intersection point.
+ const float t =
+ (plane_distance_to_origin - ray_origin.dot(plane_normal)) / d_dot_n;
+
+ // Do not count back-intersection, i.e., the intersection is in the negative
+ // |ray_direction|.
+ if (t < 0.0f) {
+ return false;
+ }
+
+ *intersection = ray_origin + t * ray_direction;
+ return true;
+}
+
+// A helper function to find the index of the wall with which the ray
+// intersects. Returns true if such wall is found.
+bool FindIntersectingWallIndex(const WorldPosition& dimensions,
+ const WorldPosition& origin,
+ const WorldPosition& direction,
+ size_t* wall_index) {
+ // The ray intersects a wall if:
+ // 1. It intersects the plane of the wall.
+ // 2. The intersection point lies in the bounding box of the wall.
+ const float dx = dimensions[0] * 0.5f;
+ const float dy = dimensions[1] * 0.5f;
+ const float dz = dimensions[2] * 0.5f;
+
+ // Data used to test plane intersections (1. above): normals and distances
+ // of all the walls.
+ const float wall_distances[kNumRoomSurfaces] = {dx, dx, dy, dy, dz, dz};
+
+ // Data used to test whether the intersecting point is in the bounding box
+ // of the wall (2. above). The bounding box has a small thickness
+ // |wall_thickness| along the normal direction of the wall.
+ const float wall_thickness = AcousticRay::kRayEpsilon;
+
+ // The centers and corresponding dimensions defining the bounding boxes of
+ // all the walls.
+ const WorldPosition wall_centers[kNumRoomSurfaces] = {
+ {-dx, 0.0f, 0.0f}, {+dx, 0.0f, 0.0f}, {0.0f, -dy, 0.0f},
+ {0.0f, +dy, 0.0f}, {0.0f, 0.0f, -dz}, {0.0f, 0.0f, +dz},
+ };
+ const WorldPosition wall_dimensions[kNumRoomSurfaces] = {
+ {wall_thickness, dimensions[1], dimensions[2]},
+ {wall_thickness, dimensions[1], dimensions[2]},
+ {dimensions[0], wall_thickness, dimensions[2]},
+ {dimensions[0], wall_thickness, dimensions[2]},
+ {dimensions[0], dimensions[1], wall_thickness},
+ {dimensions[0], dimensions[1], wall_thickness},
+ };
+
+ // Iterate through all the walls to find the one that the ray intersects with.
+ for (size_t wall = 0; wall < kNumRoomSurfaces; ++wall) {
+ WorldPosition intersection;
+ if (PlaneIntersection(WorldPosition(kCubeWallNormals[wall]),
+ wall_distances[wall], origin, direction,
+ &intersection) &&
+ IsPositionInAabb(intersection, wall_centers[wall],
+ wall_dimensions[wall])) {
+ *wall_index = wall;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// A helper function to sort a vector of {position, distance} pairs according
+// to the distances and exclude outliers.
+std::vector<std::pair<WorldPosition, float>>
+SortPositionsAndDistancesAndFindInliers(
+ float outlier_portion, const std::vector<std::pair<WorldPosition, float>>&
+ positions_and_distances) {
+ std::vector<std::pair<WorldPosition, float>>
+ sorted_inlier_positions_and_distances(positions_and_distances);
+
+ std::sort(sorted_inlier_positions_and_distances.begin(),
+ sorted_inlier_positions_and_distances.end(),
+ [](const std::pair<WorldPosition, float>& position_and_distance_1,
+ const std::pair<WorldPosition, float>& position_and_distance_2) {
+ return position_and_distance_1.second <
+ position_and_distance_2.second;
+ });
+
+ const float total_size =
+ static_cast<float>(sorted_inlier_positions_and_distances.size());
+ const size_t inliner_begin =
+ static_cast<size_t>(std::floor(total_size * outlier_portion));
+ const size_t inliner_end =
+ static_cast<size_t>(std::ceil(total_size * (1.0f - outlier_portion)));
+
+ std::copy(sorted_inlier_positions_and_distances.begin() + inliner_begin,
+ sorted_inlier_positions_and_distances.begin() + inliner_end,
+ sorted_inlier_positions_and_distances.begin());
+ sorted_inlier_positions_and_distances.resize(inliner_end - inliner_begin);
+
+ return sorted_inlier_positions_and_distances;
+}
+
+// A helper function to compute the average position and distance from a
+// vector of positions and distances.
+void ComputeAveragePositionAndDistance(
+ const std::vector<std::pair<WorldPosition, float>>& positions_and_distances,
+ WorldPosition* average_position, float* average_distance) {
+ DCHECK(!positions_and_distances.empty());
+ WorldPosition sum_positions(0.0f, 0.0f, 0.0f);
+ float sum_distances = 0.0f;
+ for (const std::pair<WorldPosition, float>& position_and_distance :
+ positions_and_distances) {
+ sum_positions += position_and_distance.first;
+ sum_distances += position_and_distance.second;
+ }
+
+ const float inverse_size =
+ 1.0f / static_cast<float>(positions_and_distances.size());
+ *average_position = sum_positions * inverse_size;
+ *average_distance = sum_distances * inverse_size;
+}
+
+} // namespace
+
+void ProxyRoomEstimator::CollectHitPointData(
+ const std::vector<Path>& paths_batch) {
+ // The size of already collected hit points before this batch.
+ const size_t previous_size = hit_points_.size();
+ hit_points_.resize(previous_size + paths_batch.size());
+
+ // Collect one hit point per path.
+ for (size_t path_index = 0; path_index < paths_batch.size(); ++path_index) {
+ const size_t hit_point_index = path_index + previous_size;
+ const Path& path = paths_batch.at(path_index);
+ hit_points_[hit_point_index] = CollectHitPointDataFromPath(path);
+ }
+}
+
+bool ProxyRoomEstimator::EstimateCubicProxyRoom(
+ float outlier_portion, RoomProperties* room_properties) {
+ // Check that |outlier_portion| is in the range of [0, 0.5].
+ CHECK_GE(outlier_portion, 0.0f);
+ CHECK_LE(outlier_portion, 0.5f);
+
+ // Estimation fails if there is no hit point.
+ if (hit_points_.empty()) {
+ return false;
+ }
+
+ // The geometry part (position, dimensions, and rotation) of the proxy room.
+ if (!EstimateCubeGeometry(outlier_portion, room_properties->position,
+ room_properties->dimensions,
+ room_properties->rotation)) {
+ LOG(WARNING) << "Unable to estimate a cube without a hit point with finite "
+ << "|t_far|; returning a default cube.";
+ return false;
+ }
+
+ // The surface material part of the proxy room.
+ EstimateSurfaceMaterials(WorldPosition(room_properties->position),
+ WorldPosition(room_properties->dimensions),
+ room_properties->material_names);
+ return true;
+}
+
+ProxyRoomEstimator::HitPointData
+ProxyRoomEstimator::CollectHitPointDataFromPath(const Path& path) {
+ CHECK(!path.rays.empty());
+ HitPointData hit_point;
+
+ // Common data.
+ const AcousticRay& first_order_ray = path.rays[0];
+ hit_point.origin = WorldPosition(first_order_ray.origin());
+ hit_point.direction = WorldPosition(first_order_ray.direction());
+
+ // Treating escaped and non-escaped rays differently for their |t_far| and
+ // |absorption_coefficients|.
+ if (path.rays.size() > 1) {
+ hit_point.t_far = first_order_ray.t_far();
+
+ // Figure out the absorption coefficients by examining the incoming and the
+ // reflected energy.
+ const AcousticRay& second_order_ray = path.rays[1];
+ for (size_t i = 0; i < kNumReverbOctaveBands; ++i) {
+ const float incoming_energy = first_order_ray.energies().at(i);
+ const float reflected_energy = second_order_ray.energies().at(i);
+ CHECK_GT(incoming_energy, 0.0f);
+ CHECK_GE(incoming_energy, reflected_energy);
+
+ hit_point.absorption_coefficients[i] =
+ 1.0f - (reflected_energy / incoming_energy);
+ }
+ } else {
+ hit_point.t_far = AcousticRay::kInfinity;
+
+ // Escaped rays are considered as completely absorbed.
+ for (size_t i = 0; i < kNumReverbOctaveBands; ++i) {
+ hit_point.absorption_coefficients[i] = 1.0f;
+ }
+ }
+
+ return hit_point;
+}
+
+bool ProxyRoomEstimator::EstimateCubeGeometry(float outlier_portion,
+ float* position,
+ float* dimensions,
+ float* rotation) {
+ // First group the hit points according to which cube wall it would hit,
+ // assuming an initial guess of a cube that
+ // 1. centers at the origin of the first ray,
+ // 2. has unit dimensions, and
+ // 3. has no rotation.
+ const WorldPosition cube_dimensions(1.0f, 1.0f, 1.0f);
+ const std::array<std::vector<HitPointData>, kNumRoomSurfaces>
+ hit_points_on_walls =
+ GroupHitPointsByWalls(hit_points_[0].origin, cube_dimensions);
+
+ // Compute the positions and distances of from the hit points on walls.
+ const std::vector<std::pair<WorldPosition, float>> positions_and_distances =
+ ComputeDistancesAndPositionsFromHitPoints(hit_points_on_walls);
+
+ // Cannot estimate if there are no valid positions and distances.
+ if (positions_and_distances.empty()) {
+ return false;
+ }
+
+ // Sort the |positions_and_distances| according to the distances and also
+ // filter out outliers (whose distances are too large or too small).
+ const std::vector<std::pair<WorldPosition, float>>
+ sorted_inlier_positions_and_distances =
+ SortPositionsAndDistancesAndFindInliers(outlier_portion,
+ positions_and_distances);
+
+ // Cannot estimate if there are no inlying positions and distances.
+ if (sorted_inlier_positions_and_distances.empty()) {
+ return false;
+ }
+
+ // Take the average distance and origin of the inlier hit points.
+ WorldPosition average_hit_point_poisition;
+ float average_distance;
+ ComputeAveragePositionAndDistance(sorted_inlier_positions_and_distances,
+ &average_hit_point_poisition,
+ &average_distance);
+
+ // Use twice the average distance as the cube's dimensions.
+ const float estimated_cube_dimension = 2.0f * average_distance;
+ dimensions[0] = estimated_cube_dimension;
+ dimensions[1] = estimated_cube_dimension;
+ dimensions[2] = estimated_cube_dimension;
+
+ // Use the average hit point position as the cube's position.
+ position[0] = average_hit_point_poisition[0];
+ position[1] = average_hit_point_poisition[1];
+ position[2] = average_hit_point_poisition[2];
+
+ // No rotation.
+ rotation[0] = 0.0f;
+ rotation[1] = 0.0f;
+ rotation[2] = 0.0f;
+ rotation[3] = 1.0f;
+
+ return true;
+}
+
+std::array<std::vector<ProxyRoomEstimator::HitPointData>, kNumRoomSurfaces>
+ProxyRoomEstimator::GroupHitPointsByWalls(
+ const WorldPosition& room_position, const WorldPosition& room_dimensions) {
+ const WorldPosition kOrigin(0.0f, 0.0f, 0.0f);
+
+ // No rotation for an axis-aligned room.
+ const WorldRotation kNoRotation(1.0f, 0.0f, 0.0f, 0.0f);
+
+ // Reserve memory space for hit points on walls.
+ std::array<std::vector<HitPointData>, kNumRoomSurfaces> hit_points_on_walls;
+ for (std::vector<HitPointData>& hit_points_on_wall : hit_points_on_walls) {
+ hit_points_on_wall.reserve(hit_points_.size());
+ }
+
+ for (const HitPointData& hit_point : hit_points_) {
+ // First transform the ray's origin and direction to the local space
+ // of the cube centered at |position|.
+ WorldPosition local_direction;
+ GetRelativeDirection(kOrigin, kNoRotation, hit_point.direction,
+ &local_direction);
+ WorldPosition local_origin;
+ GetRelativeDirection(room_position, kNoRotation, hit_point.origin,
+ &local_origin);
+
+ // Find the wall that the ray intersects.
+ size_t wall_index;
+ if (FindIntersectingWallIndex(room_dimensions, local_origin,
+ local_direction, &wall_index)) {
+ hit_points_on_walls[wall_index].push_back(hit_point);
+ }
+ }
+
+ return hit_points_on_walls;
+}
+
+std::vector<std::pair<WorldPosition, float>>
+ProxyRoomEstimator::ComputeDistancesAndPositionsFromHitPoints(
+ const std::array<std::vector<HitPointData>, kNumRoomSurfaces>&
+ hit_points_on_walls) {
+ std::vector<std::pair<WorldPosition, float>> positions_and_distances;
+ for (size_t wall = 0; wall < kNumRoomSurfaces; ++wall) {
+ const WorldPosition wall_normal(kCubeWallNormals[wall]);
+ for (const HitPointData& hit_point : hit_points_on_walls[wall]) {
+ if (hit_point.t_far == AcousticRay::kInfinity) {
+ continue;
+ }
+
+ std::pair<WorldPosition, float> position_and_distance;
+ position_and_distance.first =
+ hit_point.origin + (hit_point.t_far * hit_point.direction);
+ position_and_distance.second =
+ (position_and_distance.first - hit_point.origin).dot(wall_normal);
+ positions_and_distances.push_back(position_and_distance);
+ }
+ }
+
+ return positions_and_distances;
+}
+
+void ProxyRoomEstimator::EstimateSurfaceMaterials(
+ const WorldPosition& room_position, const WorldPosition& room_dimensions,
+ MaterialName* material_names) {
+ // Compute the average absorption coefficients on all the walls.
+ CoefficientsVector average_absorption_coefficients[kNumRoomSurfaces];
+
+ // Now that we have better estimates of the proxy room's position, dimensions,
+ // and rotation, re-group the hit points using these estimates.
+ const std::array<std::vector<HitPointData>, kNumRoomSurfaces>&
+ hit_points_on_walls =
+ GroupHitPointsByWalls(room_position, room_dimensions);
+ for (size_t wall = 0; wall < kNumRoomSurfaces; ++wall) {
+ const std::vector<HitPointData>& hit_points = hit_points_on_walls[wall];
+ for (const HitPointData& hit_point : hit_points) {
+ average_absorption_coefficients[wall] +=
+ CoefficientsVector(hit_point.absorption_coefficients.data());
+ }
+ }
+
+ for (size_t wall = 0; wall < kNumRoomSurfaces; ++wall) {
+ if (!hit_points_on_walls[wall].empty()) {
+ average_absorption_coefficients[wall] /=
+ static_cast<float>(hit_points_on_walls[wall].size());
+ } else {
+ // If no hit point is found on this wall, we consider all energy absorbed
+ // in this direction and the absorption coefficient being 1.0.
+ average_absorption_coefficients[wall].fill(1.0f);
+ }
+ }
+
+ // Find the closest surface material. The distance between two materials is
+ // defined as the simple Euclidean distance.
+ for (size_t wall = 0; wall < kNumRoomSurfaces; ++wall) {
+ MaterialName min_distance_material_name = MaterialName::kTransparent;
+ float min_material_distance = std::numeric_limits<float>::infinity();
+
+ // We want to exclude kUniform from fitting.
+ const size_t num_materials_to_fit =
+ static_cast<size_t>(MaterialName::kNumMaterialNames) - 1;
+ for (size_t material_index = 0; material_index < num_materials_to_fit;
+ ++material_index) {
+ const RoomMaterial& material = GetRoomMaterial(material_index);
+ const float material_distance =
+ (average_absorption_coefficients[wall] -
+ CoefficientsVector(material.absorption_coefficients))
+ .norm();
+ if (material_distance < min_material_distance) {
+ min_material_distance = material_distance;
+ min_distance_material_name = material.name;
+ }
+ }
+ material_names[wall] = min_distance_material_name;
+ LOG(INFO) << "Wall[" << wall << "] material= " << min_distance_material_name
+ << " distance= " << min_material_distance;
+ }
+}
+
+} // namespace vraudio