blender/intern/cycles/scene/alembic.h
Brecht Van Lommel 9cfc7967dd Cycles: use SPDX license headers
* Replace license text in headers with SPDX identifiers.
* Remove specific license info from outdated readme.txt, instead leave details
  to the source files.
* Add list of SPDX license identifiers used, and corresponding license texts.
* Update copyright dates while we're at it.

Ref D14069, T95597
2022-02-11 17:47:34 +01:00

572 lines
17 KiB
C++

/* SPDX-License-Identifier: Apache-2.0
* Copyright 2011-2022 Blender Foundation */
#pragma once
#include "graph/node.h"
#include "scene/attribute.h"
#include "scene/procedural.h"
#include "util/set.h"
#include "util/transform.h"
#include "util/vector.h"
#ifdef WITH_ALEMBIC
# include <Alembic/AbcCoreFactory/All.h>
# include <Alembic/AbcGeom/All.h>
CCL_NAMESPACE_BEGIN
class AlembicProcedural;
class Geometry;
class Object;
class Progress;
class Shader;
using MatrixSampleMap = std::map<Alembic::Abc::chrono_t, Alembic::Abc::M44d>;
struct MatrixSamplesData {
MatrixSampleMap *samples = nullptr;
Alembic::AbcCoreAbstract::TimeSamplingPtr time_sampling;
};
/* Helpers to detect if some type is a `ccl::array`. */
template<typename> struct is_array : public std::false_type {
};
template<typename T> struct is_array<array<T>> : public std::true_type {
};
/* Holds the data for a cache lookup at a given time, as well as information to
* help disambiguate successes or failures to get data from the cache. */
template<typename T> class CacheLookupResult {
enum class State {
NEW_DATA,
ALREADY_LOADED,
NO_DATA_FOR_TIME,
};
T *data;
State state;
protected:
/* Prevent default construction outside of the class: for a valid result, we
* should use the static functions below. */
CacheLookupResult() = default;
public:
static CacheLookupResult new_data(T *data_)
{
CacheLookupResult result;
result.data = data_;
result.state = State::NEW_DATA;
return result;
}
static CacheLookupResult no_data_found_for_time()
{
CacheLookupResult result;
result.data = nullptr;
result.state = State::NO_DATA_FOR_TIME;
return result;
}
static CacheLookupResult already_loaded()
{
CacheLookupResult result;
result.data = nullptr;
result.state = State::ALREADY_LOADED;
return result;
}
/* This should only be call if new data is available. */
const T &get_data() const
{
assert(state == State::NEW_DATA);
assert(data != nullptr);
return *data;
}
T *get_data_or_null() const
{
// data_ should already be null if there is no new data so no need to check
return data;
}
bool has_new_data() const
{
return state == State::NEW_DATA;
}
bool has_already_loaded() const
{
return state == State::ALREADY_LOADED;
}
bool has_no_data_for_time() const
{
return state == State::NO_DATA_FOR_TIME;
}
};
/* Store the data set for an animation at every time points, or at the beginning of the animation
* for constant data.
*
* The data is supposed to be stored in chronological order, and is looked up using the current
* animation time in seconds using the TimeSampling from the Alembic property. */
template<typename T> class DataStore {
/* Holds information to map a cache entry for a given time to an index into the data array. */
struct TimeIndexPair {
/* Frame time for this entry. */
double time = 0;
/* Frame time for the data pointed to by `index`. */
double source_time = 0;
/* Index into the data array. */
size_t index = 0;
};
/* This is the actual data that is stored. We deduplicate data across frames to avoid storing
* values if they have not changed yet (e.g. the triangles for a building before fracturing, or a
* fluid simulation before a break or splash) */
vector<T> data{};
/* This is used to map they entry for a given time to an index into the data array, multiple
* frames can point to the same index. */
vector<TimeIndexPair> index_data_map{};
Alembic::AbcCoreAbstract::TimeSampling time_sampling{};
double last_loaded_time = std::numeric_limits<double>::max();
public:
/* Keys used to compare values. */
Alembic::AbcCoreAbstract::ArraySample::Key key1;
Alembic::AbcCoreAbstract::ArraySample::Key key2;
void set_time_sampling(Alembic::AbcCoreAbstract::TimeSampling time_sampling_)
{
time_sampling = time_sampling_;
}
Alembic::AbcCoreAbstract::TimeSampling get_time_sampling() const
{
return time_sampling;
}
/* Get the data for the specified time.
* Return nullptr if there is no data or if the data for this time was already loaded. */
CacheLookupResult<T> data_for_time(double time)
{
if (size() == 0) {
return CacheLookupResult<T>::no_data_found_for_time();
}
const TimeIndexPair &index = get_index_for_time(time);
if (index.index == -1ul) {
return CacheLookupResult<T>::no_data_found_for_time();
}
if (last_loaded_time == index.time || last_loaded_time == index.source_time) {
return CacheLookupResult<T>::already_loaded();
}
last_loaded_time = index.source_time;
assert(index.index < data.size());
return CacheLookupResult<T>::new_data(&data[index.index]);
}
/* get the data for the specified time, but do not check if the data was already loaded for this
* time return nullptr if there is no data */
CacheLookupResult<T> data_for_time_no_check(double time)
{
if (size() == 0) {
return CacheLookupResult<T>::no_data_found_for_time();
}
const TimeIndexPair &index = get_index_for_time(time);
if (index.index == -1ul) {
return CacheLookupResult<T>::no_data_found_for_time();
}
assert(index.index < data.size());
return CacheLookupResult<T>::new_data(&data[index.index]);
}
void add_data(T &data_, double time)
{
index_data_map.push_back({time, time, data.size()});
if constexpr (is_array<T>::value) {
data.emplace_back();
data.back().steal_data(data_);
return;
}
data.push_back(data_);
}
void reuse_data_for_last_time(double time)
{
const TimeIndexPair &data_index = index_data_map.back();
index_data_map.push_back({time, data_index.source_time, data_index.index});
}
void add_no_data(double time)
{
index_data_map.push_back({time, time, -1ul});
}
bool is_constant() const
{
return data.size() <= 1;
}
size_t size() const
{
return data.size();
}
void clear()
{
invalidate_last_loaded_time();
data.clear();
index_data_map.clear();
}
void invalidate_last_loaded_time()
{
last_loaded_time = std::numeric_limits<double>::max();
}
/* Copy the data for the specified time to the node's socket. If there is no
* data for this time or it was already loaded, do nothing. */
void copy_to_socket(double time, Node *node, const SocketType *socket)
{
CacheLookupResult<T> result = data_for_time(time);
if (!result.has_new_data()) {
return;
}
/* TODO(kevindietrich): arrays are emptied when passed to the sockets, so for now we copy the
* arrays to avoid reloading the data */
T value = result.get_data();
node->set(*socket, value);
}
size_t memory_used() const
{
if constexpr (is_array<T>::value) {
size_t mem_used = 0;
for (const T &array : data) {
mem_used += array.size() * sizeof(array[0]);
}
return mem_used;
}
return data.size() * sizeof(T);
}
private:
const TimeIndexPair &get_index_for_time(double time) const
{
std::pair<size_t, Alembic::Abc::chrono_t> index_pair;
index_pair = time_sampling.getNearIndex(time, index_data_map.size());
return index_data_map[index_pair.first];
}
};
/* Actual cache for the stored data.
* This caches the topological, transformation, and attribute data for a Mesh node or a Hair node
* inside of DataStores.
*/
struct CachedData {
DataStore<Transform> transforms{};
/* mesh data */
DataStore<array<float3>> vertices;
DataStore<array<int3>> triangles{};
/* triangle "loops" are the polygons' vertices indices used for indexing face varying attributes
* (like UVs) */
DataStore<array<int>> uv_loops{};
DataStore<array<int>> shader{};
/* subd data */
DataStore<array<int>> subd_start_corner;
DataStore<array<int>> subd_num_corners;
DataStore<array<bool>> subd_smooth;
DataStore<array<int>> subd_ptex_offset;
DataStore<array<int>> subd_face_corners;
DataStore<int> num_ngons;
DataStore<array<int>> subd_creases_edge;
DataStore<array<float>> subd_creases_weight;
DataStore<array<int>> subd_vertex_crease_indices;
DataStore<array<float>> subd_vertex_crease_weights;
/* hair data */
DataStore<array<float3>> curve_keys;
DataStore<array<float>> curve_radius;
DataStore<array<int>> curve_first_key;
DataStore<array<int>> curve_shader;
/* point data */
DataStore<array<float3>> points;
DataStore<array<float>> radiuses;
DataStore<array<int>> points_shader;
struct CachedAttribute {
AttributeStandard std;
AttributeElement element;
TypeDesc type_desc;
ustring name;
DataStore<array<char>> data{};
};
vector<CachedAttribute> attributes{};
void clear();
CachedAttribute &add_attribute(const ustring &name,
const Alembic::Abc::TimeSampling &time_sampling);
bool is_constant() const;
void invalidate_last_loaded_time(bool attributes_only = false);
void set_time_sampling(Alembic::AbcCoreAbstract::TimeSampling time_sampling);
size_t memory_used() const;
};
/* Representation of an Alembic object for the AlembicProcedural.
*
* The AlembicObject holds the path to the Alembic IObject inside of the archive that is desired
* for rendering, as well as the list of shaders that it is using.
*
* The names of the shaders should correspond to the names of the FaceSets inside of the Alembic
* archive for per-triangle shader association. If there is no FaceSets, or the names do not
* match, the first shader is used for rendering for all triangles.
*/
class AlembicObject : public Node {
public:
NODE_DECLARE
/* Path to the IObject inside of the archive. */
NODE_SOCKET_API(ustring, path)
/* Shaders used for rendering. */
NODE_SOCKET_API_ARRAY(array<Node *>, used_shaders)
/* Treat this subdivision object as a regular polygon mesh, so no subdivision will be performed.
*/
NODE_SOCKET_API(bool, ignore_subdivision)
/* Maximum number of subdivisions for ISubD objects. */
NODE_SOCKET_API(int, subd_max_level)
/* Finest level of detail (in pixels) for the subdivision. */
NODE_SOCKET_API(float, subd_dicing_rate)
/* Scale the radius of points and curves. */
NODE_SOCKET_API(float, radius_scale)
AlembicObject();
~AlembicObject();
private:
friend class AlembicProcedural;
void set_object(Object *object);
Object *get_object();
void load_data_in_cache(CachedData &cached_data,
AlembicProcedural *proc,
Alembic::AbcGeom::IPolyMeshSchema &schema,
Progress &progress);
void load_data_in_cache(CachedData &cached_data,
AlembicProcedural *proc,
Alembic::AbcGeom::ISubDSchema &schema,
Progress &progress);
void load_data_in_cache(CachedData &cached_data,
AlembicProcedural *proc,
const Alembic::AbcGeom::ICurvesSchema &schema,
Progress &progress);
bool has_data_loaded() const;
/* Enumeration used to speed up the discrimination of an IObject as IObject::matches() methods
* are too expensive and show up in profiles. */
enum AbcSchemaType {
INVALID,
POLY_MESH,
SUBD,
CURVES,
POINTS,
};
bool need_shader_update = true;
AlembicObject *instance_of = nullptr;
Alembic::AbcCoreAbstract::TimeSamplingPtr xform_time_sampling;
MatrixSampleMap xform_samples;
Alembic::AbcGeom::IObject iobject;
/* Set if the path points to a valid IObject whose type is supported. */
AbcSchemaType schema_type;
CachedData &get_cached_data()
{
return cached_data_;
}
bool is_constant() const
{
return cached_data_.is_constant();
}
void clear_cache()
{
cached_data_.clear();
}
Object *object = nullptr;
bool data_loaded = false;
CachedData cached_data_;
void setup_transform_cache(CachedData &cached_data, float scale);
AttributeRequestSet get_requested_attributes();
};
/* Procedural to render objects from a single Alembic archive.
*
* Every object desired to be rendered should be passed as an AlembicObject through the objects
* socket.
*
* This procedural will load the data set for the entire animation in memory on the first frame,
* and directly set the data for the new frames on the created Nodes if needed. This allows for
* faster updates between frames as it avoids reseeking the data on disk.
*/
class AlembicProcedural : public Procedural {
Alembic::AbcGeom::IArchive archive;
bool objects_loaded;
Scene *scene_;
public:
NODE_DECLARE
/* The file path to the Alembic archive */
NODE_SOCKET_API(ustring, filepath)
/* Layers for the Alembic archive. Layers are in the order in which they override data, with the
* latter elements overriding the former ones. */
NODE_SOCKET_API_ARRAY(array<ustring>, layers)
/* The current frame to render. */
NODE_SOCKET_API(float, frame)
/* The first frame to load data for. */
NODE_SOCKET_API(float, start_frame)
/* The last frame to load data for. */
NODE_SOCKET_API(float, end_frame)
/* Subtracted to the current frame. */
NODE_SOCKET_API(float, frame_offset)
/* The frame rate used for rendering in units of frames per second. */
NODE_SOCKET_API(float, frame_rate)
/* List of AlembicObjects to render. */
NODE_SOCKET_API_ARRAY(array<Node *>, objects)
/* Set the default radius to use for curves when the Alembic Curves Schemas do not have radius
* information. */
NODE_SOCKET_API(float, default_radius)
/* Multiplier to account for differences in default units for measuring objects in various
* software. */
NODE_SOCKET_API(float, scale)
/* Cache controls */
NODE_SOCKET_API(bool, use_prefetch)
/* Memory limit for the cache, if the data does not fit within this limit, rendering is aborted.
*/
NODE_SOCKET_API(int, prefetch_cache_size)
AlembicProcedural();
~AlembicProcedural();
/* Populates the Cycles scene with Nodes for every contained AlembicObject on the first
* invocation, and updates the data on subsequent invocations if the frame changed. */
void generate(Scene *scene, Progress &progress);
/* Tag for an update only if something was modified. */
void tag_update(Scene *scene);
/* This should be called by scene exporters to request the rendering of an object located
* in the Alembic archive at the given path.
*
* Since we lazily load object, the function does not validate the existence of the object
* in the archive. If no objects with such path if found in the archive during the next call
* to `generate`, it will be ignored.
*
* Returns a pointer to an existing or a newly created AlembicObject for the given path. */
AlembicObject *get_or_create_object(const ustring &path);
private:
/* Add an object to our list of objects, and tag the socket as modified. */
void add_object(AlembicObject *object);
/* Load the data for all the objects whose data has not yet been loaded. */
void load_objects(Progress &progress);
/* Traverse the Alembic hierarchy to lookup the IObjects for the AlembicObjects that were
* specified in our objects socket, and accumulate all of the transformations samples along the
* way for each IObject. */
void walk_hierarchy(Alembic::AbcGeom::IObject parent,
const Alembic::AbcGeom::ObjectHeader &ohead,
MatrixSamplesData matrix_samples_data,
const unordered_map<string, AlembicObject *> &object_map,
Progress &progress);
/* Read the data for an IPolyMesh at the specified frame_time. Creates corresponding Geometry and
* Object Nodes in the Cycles scene if none exist yet. */
void read_mesh(AlembicObject *abc_object, Alembic::AbcGeom::Abc::chrono_t frame_time);
/* Read the data for an ICurves at the specified frame_time. Creates corresponding Geometry and
* Object Nodes in the Cycles scene if none exist yet. */
void read_curves(AlembicObject *abc_object, Alembic::AbcGeom::Abc::chrono_t frame_time);
/* Read the data for an IPoints at the specified frame_time. Creates corresponding Geometry and
* Object Nodes in the Cycles scene if none exist yet. */
void read_points(AlembicObject *abc_object, Alembic::AbcGeom::Abc::chrono_t frame_time);
/* Read the data for an ISubD at the specified frame_time. Creates corresponding Geometry and
* Object Nodes in the Cycles scene if none exist yet. */
void read_subd(AlembicObject *abc_object, Alembic::AbcGeom::Abc::chrono_t frame_time);
void build_caches(Progress &progress);
size_t get_prefetch_cache_size_in_bytes() const
{
/* prefetch_cache_size is in megabytes, so convert to bytes. */
return static_cast<size_t>(prefetch_cache_size) * 1024 * 1024;
}
};
CCL_NAMESPACE_END
#endif