diff options
Diffstat (limited to 'o3d/core/cross/curve.cc')
-rw-r--r-- | o3d/core/cross/curve.cc | 806 |
1 files changed, 806 insertions, 0 deletions
diff --git a/o3d/core/cross/curve.cc b/o3d/core/cross/curve.cc new file mode 100644 index 0000000..9aff37f --- /dev/null +++ b/o3d/core/cross/curve.cc @@ -0,0 +1,806 @@ +/* + * Copyright 2009, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +// This file contains the implementation of Curve. +// This code is heavily influenced by code from FCollada. + +#include "core/cross/precompile.h" +#include "core/cross/curve.h" +#include "core/cross/error.h" +#include "import/cross/memory_stream.h" +#include "import/cross/raw_data.h" + +#undef min +#undef max + +namespace o3d { + +O3D_DEFN_CLASS(CurveKey, ObjectBase); +O3D_DEFN_CLASS(StepCurveKey, CurveKey); +O3D_DEFN_CLASS(LinearCurveKey, CurveKey); +O3D_DEFN_CLASS(BezierCurveKey, CurveKey); +O3D_DEFN_CLASS(CurveFunctionContext, FunctionContext); +O3D_DEFN_CLASS(Curve, Function); + +const char *Curve::kSerializationID = "CURV"; + +namespace { + +const float kEpsilon = 0.00001f; + +float Clamp(float value, float min_value, float max_value) { + return value > max_value ? max_value : + (value < min_value ? min_value : value); +} + +// Uses iterative method to accurately pin-point the 't' of the Bezier +// equation that corresponds to the current time. +float FindT(float control_point_0_x, + float control_point_1_x, + float control_point_2_x, + float control_point_3_x, + float input, + float initial_guess) { + float local_tolerance = 0.001f; + float high_t = 1.0f; + float low_t = 0.0f; + + // TODO: Optimize here, start with a more intuitive value than 0.5 + // (comment left over from original code) + float mid_t = 0.5f; + if (initial_guess <= 0.1) { + mid_t = 0.1f; // clamp to 10% or 90%, because if miss, the cost is + // too high. + } else if (initial_guess >= 0.9) { + mid_t = 0.9f; + } else { + mid_t = initial_guess; + } + bool once = true; + while ((high_t-low_t) > local_tolerance) { + if (once) { + once = false; + } else { + mid_t = (high_t - low_t) / 2.0f + low_t; + } + float ti = 1.0f - mid_t; // (1 - t) + float calculated_time = control_point_0_x * ti * ti * ti + + 3 * control_point_1_x * mid_t * ti * ti + + 3 * control_point_2_x * mid_t * mid_t * ti + + control_point_3_x * mid_t * mid_t * mid_t; + if (fabsf(calculated_time - input) <= local_tolerance) { + break; // If we 'fall' very close, we like it and break. + } + if (calculated_time > input) { + high_t = mid_t; + } else { + low_t = mid_t; + } + } + return mid_t; +} + +typedef CurveKey::Ref (*KeyCreatorFunc)(ServiceLocator* service_locator, + Curve* onwer); +struct KeyCreator { + const ObjectBase::Class* key_type; + KeyCreatorFunc create_function; +}; +static KeyCreator g_creators[] = { + { StepCurveKey::GetApparentClass(), StepCurveKey::Create, }, + { LinearCurveKey::GetApparentClass(), LinearCurveKey::Create, }, + { BezierCurveKey::GetApparentClass(), BezierCurveKey::Create, }, +}; + +} // anonymous namespace + +CurveKey::CurveKey(ServiceLocator* service_locator, Curve* owner) + : ObjectBase(service_locator), + owner_(owner), + input_(0.0f), + output_(0.0f) { +} + +void CurveKey::Destroy() { + owner_->RemoveKey(this); +} + +void CurveKey::SetInput(float new_input) { + if (new_input != input_) { + input_ = new_input; + owner_->MarkAsUnsorted(); + } +} + +void CurveKey::SetOutput(float new_output) { + if (new_output != output_) { + output_ = new_output; + owner_->InvalidateCache(); + } +} + +// StepCurveKey ------------------------------------------------------------ + +StepCurveKey::StepCurveKey(ServiceLocator* service_locator, Curve* owner) + : CurveKey(service_locator, owner) { +} + +CurveKey::Ref StepCurveKey::Create(ServiceLocator* service_locator, + Curve* owner) { + return CurveKey::Ref(new StepCurveKey(service_locator, owner)); +} + +float StepCurveKey::GetOutputAtOffset(float offset, unsigned key_index) const { + return output(); +} + +// LinearCurveKey ---------------------------------------------------------- + +LinearCurveKey::LinearCurveKey(ServiceLocator* service_locator, Curve* owner) + : CurveKey(service_locator, owner) { +} + +CurveKey::Ref LinearCurveKey::Create(ServiceLocator* service_locator, + Curve* owner) { + return CurveKey::Ref(new LinearCurveKey(service_locator, owner)); +} + +float LinearCurveKey::GetOutputAtOffset(float offset, + unsigned key_index) const { + const CurveKey* next_key(owner()->GetKey(key_index + 1)); + DCHECK(next_key); + + float input_span = next_key->input() - input(); + float output_span = next_key->output() - output(); + return output() + offset / input_span * output_span; +} + +// BezierCurveKey ---------------------------------------------------------- + +BezierCurveKey::BezierCurveKey(ServiceLocator* service_locator, Curve* owner) + : CurveKey(service_locator, owner), + in_tangent_(0, 0), + out_tangent_(0, 0) { +} + +CurveKey::Ref BezierCurveKey::Create(ServiceLocator* service_locator, + Curve* owner) { + return CurveKey::Ref(new BezierCurveKey(service_locator, owner)); +} + +void BezierCurveKey::SetInTangent(const Float2& value) { + in_tangent_ = value; + owner()->InvalidateCache(); +} + +void BezierCurveKey::SetOutTangent(const Float2& value) { + out_tangent_ = value; + owner()->InvalidateCache(); +} + +float BezierCurveKey::GetOutputAtOffset(float offset, + unsigned key_index) const { + const CurveKey* next_key(owner()->GetKey(key_index + 1)); + DCHECK(next_key); + + float input_span = next_key->input() - input(); + float output_span = next_key->output() - output(); + Float2 in_tangent; + + // We check bezier first because it's the most likely match for another + // bezier key. + if (next_key->GetClass() == BezierCurveKey::GetApparentClass()) { + in_tangent = (down_cast<const BezierCurveKey*>(next_key)->in_tangent()); + } else if (next_key->GetClass() == LinearCurveKey::GetApparentClass() || + next_key->GetClass() == StepCurveKey::GetApparentClass()) { + in_tangent.setX(next_key->input() - input_span / 3.0f); + in_tangent.setY(next_key->output() - output_span / 3.0f); + } else { + DCHECK(false); // Bad Key. + return output(); + } + + // Do a bezier calculation. + float t = offset / input_span; + t = FindT(input(), + out_tangent_.getX(), + in_tangent.getX(), + next_key->input(), + input() + offset, + t); + float b = out_tangent_.getY(); + float c = in_tangent.getY(); + float ti = 1.0f - t; + float br = 3.0f; + float cr = 3.0f; + return output() * ti * ti * ti + br * b * ti * ti * t + + cr * c * ti * t * t + next_key->output() * t * t * t; +} + +// CurveFunctionContext ------------------------------------------------ + +CurveFunctionContext::CurveFunctionContext(ServiceLocator* service_locator) + : FunctionContext(service_locator), + last_key_index_(0) { +} + +// Curve -------------------------------------------------------------- + +const float Curve::kDefaultSampleRate = 1.0f / 30.0f; // 30hz +const float Curve::kMinimumSampleRate = 1.0f / 240.0f; // 240hz + +Curve::Curve(ServiceLocator* service_locator) + : Function(service_locator), + pre_infinity_(CONSTANT), + post_infinity_(CONSTANT), + sorted_(true), + use_cache_(true), + sample_rate_(kDefaultSampleRate), + cache_valid_(false), + check_discontinuity_(false), + discontinuous_(false), + num_step_keys_(0), + last_key_index_(0) { +} + +Curve::~Curve() { +} + +const ObjectBase::Class* Curve::GetFunctionContextClass() const { + return CurveFunctionContext::GetApparentClass(); +} + +FunctionContext* Curve::CreateFunctionContext() const { + return new CurveFunctionContext(service_locator()); +} + +void Curve::AddKey(CurveKey::Ref key) { + keys_.push_back(key); + MarkAsUnsorted(); + if (key->IsA(StepCurveKey::GetApparentClass())) { + ++num_step_keys_; + } +} + +CurveKey* Curve::CreateKeyByClass(const ObjectBase::Class* key_type) { + for (unsigned ii = 0; ii < arraysize(g_creators); ++ii) { + if (g_creators[ii].key_type == key_type) { + CurveKey::Ref key(g_creators[ii].create_function( + service_locator(), + this)); + AddKey(key); + return key.Get(); + } + } + + O3D_ERROR(service_locator()) + << "unrecognized key type '" << (key_type ? key_type->name() : "NULL") + << "'"; + return NULL; +} + +CurveKey* Curve::CreateKeyByClassName(const String& key_type) { + for (unsigned ii = 0; ii < arraysize(g_creators); ++ii) { + if (!key_type.compare(g_creators[ii].key_type->name()) || + !key_type.compare(g_creators[ii].key_type->unqualified_name())) { + CurveKey::Ref key(g_creators[ii].create_function( + service_locator(), + this)); + AddKey(key); + return key.Get(); + } + } + + O3D_ERROR(service_locator()) + << "unrecognized key type '" << key_type << "'"; + return NULL; +} + +void Curve::RemoveKey(CurveKey* key) { + // keep a reference to the key so it doesn't get deleted early. + CurveKey::Ref temp(key); + CurveKeyRefArray::iterator end = std::remove(keys_.begin(), + keys_.end(), + CurveKey::Ref(key)); + + // key should never be in the key array more than once. + DLOG_ASSERT(std::distance(end, keys_.end()) <= 1); + + // The key was never found. + DCHECK(end != keys_.end()); + + if (key->IsA(StepCurveKey::GetApparentClass())) { + --num_step_keys_; + } + + // Actually remove the key from the key array + keys_.erase(end, keys_.end()); + + InvalidateCache(); + + // The key will get destroyed here since the last reference to + // it, temp, will be released. +} + +void Curve::SetSampleRate(float rate) { + if (rate < kMinimumSampleRate) { + // TODO: should we just silently set it to the minimum? + O3D_ERROR(service_locator()) + << "attempt to set sample rate to " << rate + << " which is lower than the minimum of " + << kMinimumSampleRate; + } else if (rate != sample_rate_) { + sample_rate_ = rate; + InvalidateCache(); + } +} + +void Curve::MarkAsUnsorted() const { + sorted_ = false; +} + +void Curve::InvalidateCache() const { + cache_valid_ = false; + check_discontinuity_ = true; +} + +bool Curve::IsDiscontinuous() const { + UpdateCurveInfo(); + return discontinuous_; +} + +inline static bool CompareByInput(const CurveKey::Ref& lhs, + const CurveKey::Ref& rhs) { + return lhs->input() < rhs->input(); +} + +void Curve::ResortKeys() const { + std::sort(keys_.begin(), keys_.end(), CompareByInput); + sorted_ = true; + InvalidateCache(); +} + +void Curve::CheckDiscontinuity() const { + // Mark the curve as discontinuous if any 2 keys share the same input and + // if their outputs are different. + check_discontinuity_ = false; + discontinuous_ = num_step_keys_ > 0 && num_step_keys_ != keys_.size(); + if (!discontinuous_ && keys_.size() > 1) { + for (unsigned ii = 0; ii < keys_.size() - 1; ++ii) { + if (keys_[ii]->input() == keys_[ii + 1]->input() && + keys_[ii]->output() != keys_[ii + 1]->output()) { + discontinuous_ = true; + break; + } + } + } +} + +void Curve::CreateCache() const { + float start_input = keys_.front()->input(); + float end_input = keys_.back()->input(); + float input_span = end_input - start_input; + + unsigned samples = + static_cast<unsigned>(ceilf(input_span / sample_rate_) + 1); + cache_samples_.clear(); + cache_samples_.resize(samples, 0.0f); + + FunctionContext::Ref context = FunctionContext::Ref(CreateFunctionContext()); + + for (unsigned ii = 0; ii < samples; ++ii) { + cache_samples_[ii] = GetOutputInSpan( + start_input + sample_rate_ * static_cast<float>(ii), + down_cast<CurveFunctionContext*>(context.Get())); + } + + cache_valid_ = true; +} + +float Curve::GetOutputInSpan(float input, CurveFunctionContext* context) const { + DCHECK(input >= keys_.front()->input()); + + if (input >= keys_.back()->input()) { + return keys_.back()->output(); + } + + // use the keys directly. + unsigned start = 0; + unsigned end = keys_.size(); + unsigned key_index; + bool found = false; + + static const unsigned kKeysToSearch = 3U; + + // See if the context already has a index to the correct key. + if (context) { + key_index = context->last_key_index(); + // is that index in range. + if (key_index < end - 1) { + // Are we between these keys. + if (keys_[key_index]->input() <= input && + keys_[key_index + 1]->input() > input) { + // Yes! + found = true; + } else { + // No, so check which way we need to go. + if (input > keys_[key_index]->input()) { + // search forward a few keys. If it's not within a few keys give up. + unsigned check_end = std::max(key_index + kKeysToSearch, end); + for (++key_index; key_index < check_end; ++key_index) { + if (keys_[key_index]->input() <= input && + keys_[key_index + 1]->input() > input) { + // Yes! + found = true; + break; + } + } + } else if (key_index > 0) { + // search backward a few keys. If it's not within a few keys give up. + unsigned check_end = std::min(key_index - kKeysToSearch, 0U); + for (--key_index; key_index >= check_end; --key_index) { + if (keys_[key_index]->input() <= input && + keys_[key_index + 1]->input() > input) { + // Yes! + found = true; + break; + } + } + } + } + } + } + + if (!found) { + // TODO: If we assume the most common case is sampled keys and + // constant intervals we can make a quick guess where that key is. + + // Find the current the keys that cover our input. + while (start <= end) { + unsigned mid = (start + end) / 2; + if (input > keys_[mid]->input()) { + start = mid + 1; + } else { + if (mid == 0) { + break; + } + end = mid - 1; + } + } + + end = keys_.size(); + while (start < end) { + if (keys_[start]->input() > input) { + break; + } + ++start; + } + + DCHECK(start > 0); + DCHECK(start < end); + + key_index = start - 1; + } + + const CurveKey* key = keys_[key_index]; + if (context) { + context->set_last_key_index(key_index); + } + return key->GetOutputAtOffset(input - key->input(), key_index); +} + +float Curve::Evaluate(float input, FunctionContext* context) const { + if (context && !context->IsA(CurveFunctionContext::GetApparentClass())) { + O3D_ERROR(service_locator()) + << "function context '" << context->GetClassName() + << "' is wrong type for Curve"; + context = NULL; + } + + if (keys_.empty()) { + return 0.0f; + } + + if (keys_.size() == 1) { + return keys_.front()->output(); + } + + UpdateCurveInfo(); + + float start_input = keys_.front()->input(); + float end_input = keys_.back()->input(); + float input_span = end_input - start_input; + float start_output = keys_.front()->output(); + float end_output = keys_.back()->output(); + float output_delta = end_output - start_output; + + float output_offset = 0.0f; + // check for pre-infinity + if (input < start_input) { + if (input_span <= 0.0f) { + return start_output; + } + float pre_infinity_offset = start_input - input; + switch (pre_infinity_) { + case CONSTANT: + return start_output; + case LINEAR: { + const CurveKey* second_key = keys_[1]; + float input_delta = second_key->input() - start_input; + if (input_delta > kEpsilon) { + return start_output - pre_infinity_offset * + (second_key->output() - start_output) / input_delta; + } else { + return start_output; + } + } + case CYCLE: { + float cycle_count = ceilf(pre_infinity_offset / input_span); + input += cycle_count * input_span; + input = start_input + fmodf(input - start_input, input_span); + break; + } + case CYCLE_RELATIVE: { + float cycle_count = ceilf(pre_infinity_offset / input_span); + input += cycle_count * input_span; + input = start_input + fmodf(input - start_input, input_span); + output_offset -= cycle_count * output_delta; + break; + } + case OSCILLATE: { + float cycle_count = ceilf(pre_infinity_offset / (2.0f * input_span)); + input += cycle_count * 2.0f * input_span; + input = end_input - fabsf(input - end_input); + break; + } + default: + O3D_ERROR(service_locator()) << "invalid value for pre-infinity"; + return start_output; + } + } else if (input >= end_input) { + // check for post-infinity + if (input_span <= 0.0f) { + return end_output; + } + float post_infinity_offset = input - end_input; + switch (post_infinity_) { + case CONSTANT: + return end_output; + case LINEAR: { + const CurveKey* next_to_last_key = keys_[keys_.size() - 2]; + float input_delta = end_input - next_to_last_key->input(); + if (input_delta > kEpsilon) { + return end_output + post_infinity_offset * + (end_output - next_to_last_key->output()) / + input_delta; + } else { + return end_output; + } + } + case CYCLE: { + float cycle_count = ceilf(post_infinity_offset / input_span); + input -= cycle_count * input_span; + input = start_input + fmodf(input - start_input, input_span); + break; + } + case CYCLE_RELATIVE: { + float cycle_count = floorf((input - start_input) / input_span); + input -= cycle_count * input_span; + input = start_input + fmodf(input - start_input, input_span); + output_offset += cycle_count * output_delta; + break; + } + case OSCILLATE: { + float cycle_count = ceilf(post_infinity_offset / (2.0f * + input_span)); + input -= cycle_count * 2.0f * input_span; + input = start_input + fabsf(input - start_input); + break; + } + default: + O3D_ERROR(service_locator()) << "invalid value for post-infinity"; + return end_output; + } + } + + // At this point input should be between start_input and end_input + // inclusive. + + // If we are at end_input then just return end_output since we can't + // interpolate end_input to anything past it. + if (input >= end_input) { + return end_output + output_offset; + } + + if (!discontinuous_ && use_cache_) { + // use the cache. The cache is implemented as a sampling of outputs at + // specific intervals (sample_rate_). Linear interpolation is used between + // samples. The is significantly faster than using the real keys but it + // takes a lot more memory and it can't handle discontinuous animation like + // camera cuts. + if (!cache_valid_) { + CreateCache(); + } + float span_input = input - start_input; + unsigned sample = static_cast<unsigned>(span_input / sample_rate_); + + DCHECK(sample < cache_samples_.size() - 1); + + float current_sample = cache_samples_[sample]; + if (num_step_keys_ == keys_.size()) { + // It's all step keys so don't interpolate. + return current_sample + output_offset; + } else { + float offset = fmodf(span_input, sample_rate_); + float next_sample = cache_samples_[sample + 1]; + return current_sample + (next_sample - current_sample) * + offset / sample_rate_ + output_offset; + } + } else { + return GetOutputInSpan( + input, down_cast<CurveFunctionContext*>(context)) + output_offset; + } +} + +ObjectBase::Ref Curve::Create(ServiceLocator* service_locator) { + return ObjectBase::Ref(new Curve(service_locator)); +} + + +static Float2 ReadFloat2(MemoryReadStream *stream) { + float v1 = stream->ReadLittleEndianFloat32(); + float v2 = stream->ReadLittleEndianFloat32(); + Float2 value; + value.setX(v1); + value.setY(v2); + return value; +} + +// Some constants for error checking +const size_t kFloat2Size = 2 * sizeof(float); +const size_t kStepDataSize = 2 * sizeof(float); +const size_t kLinearDataSize = 2 * sizeof(float); + +// not using sizeof(Float2) just in case this class adds ivars +// we're being more explicit here +const size_t kBezierDataSize = 2 * sizeof(float) + 2 * kFloat2Size; + +bool Curve::LoadFromBinaryData(MemoryReadStream *stream) { + // Make sure we have enough data for serialization ID and version + if (stream->GetRemainingByteCount() < 4 + sizeof(int32)) { + O3D_ERROR(service_locator()) << "invalid empty curve data"; + return false; + } + + // To insure data integrity we expect four characters kSerializationID + uint8 id[4]; + stream->Read(id, 4); + + if (memcmp(id, kSerializationID, 4)) { + O3D_ERROR(service_locator()) + << "data object does not contain curve data"; + return false; + } + + int32 version = stream->ReadLittleEndianInt32(); + if (version != 1) { + O3D_ERROR(service_locator()) << "unknown version for curve data"; + return false; + } + + while (!stream->EndOfStream()) { + // switch on types of keys + CurveKey::KeyType key_type = + static_cast<CurveKey::KeyType>(stream->ReadByte()); + size_t available_bytes = stream->GetRemainingByteCount(); + + switch (key_type) { + case CurveKey::TYPE_STEP: { + if (available_bytes < kStepDataSize) { + O3D_ERROR(service_locator()) << "unexpected end of curve data"; + return false; + } + float input = stream->ReadLittleEndianFloat32(); + float output = stream->ReadLittleEndianFloat32(); + StepCurveKey* key = Create<StepCurveKey>(); + key->SetInput(input); + key->SetOutput(output); + break; + } + case CurveKey::TYPE_LINEAR: { + if (available_bytes < kLinearDataSize) { + O3D_ERROR(service_locator()) << "unexpected end of curve data"; + return false; + } + float input = stream->ReadLittleEndianFloat32(); + float output = stream->ReadLittleEndianFloat32(); + LinearCurveKey* key = Create<LinearCurveKey>(); + key->SetInput(input); + key->SetOutput(output); + break; + } + case CurveKey::TYPE_BEZIER: { + if (available_bytes < kBezierDataSize) { + O3D_ERROR(service_locator()) << "unexpected end of curve data"; + return false; + } + float input = stream->ReadLittleEndianFloat32(); + float output = stream->ReadLittleEndianFloat32(); + Float2 in_tangent = ReadFloat2(stream); + Float2 out_tangent = ReadFloat2(stream); + BezierCurveKey* key = Create<BezierCurveKey>(); + key->SetInput(input); + key->SetOutput(output); + key->SetInTangent(in_tangent); + key->SetOutTangent(out_tangent); + break; + } + default: { + O3D_ERROR(service_locator()) << "invalid curve data"; + return false; // unknown key type + } + } + } + return true; +} + +bool Curve::Set(o3d::RawData *raw_data) { + if (!raw_data) { + O3D_ERROR(service_locator()) << "data object is null"; + return false; + } + return Set(raw_data, 0, raw_data->GetLength()); +} + +bool Curve::Set(o3d::RawData *raw_data, + size_t offset, + size_t length) { + if (!raw_data) { + O3D_ERROR(service_locator()) << "data object is null"; + return false; + } + + if (!raw_data->IsOffsetLengthValid(offset, length)) { + O3D_ERROR(service_locator()) << "illegal curve data offset or size"; + return false; + } + + const uint8 *data = raw_data->GetDataAs<uint8>(offset); + if (!data) { + return false; + } + + MemoryReadStream stream(data, length); + return LoadFromBinaryData(&stream); +} + +} // namespace o3d |