Apertura del repositorio

This commit is contained in:
2025-05-24 18:09:39 -03:00
parent 76e6359dad
commit d883ddd0d0
35253 changed files with 2891973 additions and 2 deletions
+156
View File
@@ -0,0 +1,156 @@
# Linden Lab GLTF Implementation
Currently in prototype stage. Much functionality is missing (blend shapes,
multiple texture coordinates, etc).
GLTF Specification can be found here: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html.
If this implementation disagrees with the GLTF Specification, the specification is correct.
Class structure and naming should match the GLTF Specification as closely as possible while
conforming to the LL coding standards. All code in headers should be contained in the
LL::GLTF namespace.
The implementation serves both the client and the server.
## Design Principles
- The implementation MUST be capable of round-trip serialization with no data loss beyond F64 to F32 conversions.
- The implementation MUST use the same indexing scheme as the GLTF specification. Do not store pointers where the
- GLTF specification stores indices, store indices.
- Limit dependencies on llcommon as much as possible. Prefer std::, boost::, and (soon) glm:: over LL facsimiles.
- Usage of LLSD is forbidden in the LL::GLTF namespace.
- Use "using namespace" liberally in .cpp files, but never in .h files.
- "using Foo = Bar" is permissible in .h files within the LL::GLTF namespace.
## Loading, Copying, and Serialization
Each class should provide two functions (Primitive shown for example):
```
// Serialize to the provided json object.
// "obj" should be "this" in json form on return
// Do not serialize default values
void serialize(boost::json::object& obj) const;
// Initialize from a provided json value
const Primitive& operator=(const Value& src);
```
"serialize" implementations should use "write":
```
void Primitive::serialize(boost::json::object& dst) const
{
write(mMaterial, "material", dst, -1);
write(mMode, "mode", dst, TINYGLTF_MODE_TRIANGLES);
write(mIndices, "indices", dst, INVALID_INDEX);
write(mAttributes, "attributes", dst);
}
```
And operator= implementations should use "copy":
```
const Primitive& Primitive::operator=(const Value& src)
{
if (src.is_object())
{
copy(src, "material", mMaterial);
copy(src, "mode", mMode);
copy(src, "indices", mIndices);
copy(src, "attributes", mAttributes);
mGLMode = gltf_mode_to_gl_mode(mMode);
}
return *this;
}
```
Parameters to "write" and "copy" MUST be ordered "src" before "dst"
so the code reads as "write src to dst" and "copy src to dst".
When reading string constants from GLTF json (i.e. "OPAQUE", "TRIANGLES"), these
strings should be converted to enums inside operator=. It is permissible to
store the original strings during prototyping to aid in development, but eventually
we'll purge these strings from the implementation. However, implementations MUST
preserve any and all "name" members.
"write" and "copy" implementations MUST be stored in buffer_util.h.
As implementers encounter new data types, you'll see compiler errors
pointing at templates in buffer_util.h. See vec3 as a known good
example of how to add support for a new type (there are bad examples, so beware):
```
// vec3
template<>
inline bool copy(const Value& src, vec3& dst)
{
if (src.is_array())
{
const boost::json::array& arr = src.as_array();
if (arr.size() == 3)
{
if (arr[0].is_double() &&
arr[1].is_double() &&
arr[2].is_double())
{
dst = vec3(arr[0].get_double(), arr[1].get_double(), arr[2].get_double());
}
return true;
}
}
return false;
}
template<>
inline bool write(const vec3& src, Value& dst)
{
dst = boost::json::array();
boost::json::array& arr = dst.as_array();
arr.resize(3);
arr[0] = src.x;
arr[1] = src.y;
arr[2] = src.z;
return true;
}
```
"write" MUST return true if ANY data was written
"copy" MUST return true if ANY data was copied
Speed is important, but so is safety. In writers, try to avoid redundant copies
(prefer resize over push_back, convert dst to an empty array and fill it, don't
make an array on the stack and copy it into dst).
boost::json WILL throw exceptions if you call as_foo() on a mismatched type but
WILL NOT throw exceptions on get_foo with a mismatched type. ALWAYS check is_foo
before calling as_foo or get_foo. DO NOT add exception handlers. If boost throws
an exception in serialization, the fix is to add type checks. If we see a large
number of crash reports from boost::json exceptions, each of those reports
indicates a place where we're missing "is_foo" checks. They are gold. Do not
bury them with an exception handler.
DO NOT rely on existing type conversion tools in the LL codebase -- LL data models
conflict with the GLTF specification so we MUST provide conversions independent of
our existing implementations.
### JSON Serialization ###
NEVER include buffer_util.h from a header.
Loading from and saving to disk (import/export) is currently done using tinygltf, but this is not a long term
solution. Eventually the implementation should rely solely on boost::json for reading and writing .gltf
files and should handle .bin files natively.
When serializing Images and Buffers to the server, clients MUST store a single UUID "uri" field and nothing else.
The server MUST reject any data that violates this requirement.
Clients MUST remove any Images from Buffers prior to upload to the server.
Servers MAY reject Assets that contain Buffers with unreferenced data.
... to be continued.
+298
View File
@@ -0,0 +1,298 @@
/**
* @file accessor.cpp
* @brief LL GLTF Implementation
*
* $LicenseInfo:firstyear=2024&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2024, Linden Research, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
* $/LicenseInfo$
*/
#include "../llviewerprecompiledheaders.h"
#include "asset.h"
#include "buffer_util.h"
#include "llfilesystem.h"
using namespace LL::GLTF;
using namespace boost::json;
namespace LL
{
namespace GLTF
{
Accessor::Type gltf_type_to_enum(const std::string& type)
{
if (type == "SCALAR")
{
return Accessor::Type::SCALAR;
}
else if (type == "VEC2")
{
return Accessor::Type::VEC2;
}
else if (type == "VEC3")
{
return Accessor::Type::VEC3;
}
else if (type == "VEC4")
{
return Accessor::Type::VEC4;
}
else if (type == "MAT2")
{
return Accessor::Type::MAT2;
}
else if (type == "MAT3")
{
return Accessor::Type::MAT3;
}
else if (type == "MAT4")
{
return Accessor::Type::MAT4;
}
LL_WARNS("GLTF") << "Unknown accessor type: " << type << LL_ENDL;
llassert(false);
return Accessor::Type::SCALAR;
}
std::string enum_to_gltf_type(Accessor::Type type)
{
switch (type)
{
case Accessor::Type::SCALAR:
return "SCALAR";
case Accessor::Type::VEC2:
return "VEC2";
case Accessor::Type::VEC3:
return "VEC3";
case Accessor::Type::VEC4:
return "VEC4";
case Accessor::Type::MAT2:
return "MAT2";
case Accessor::Type::MAT3:
return "MAT3";
case Accessor::Type::MAT4:
return "MAT4";
}
LL_WARNS("GLTF") << "Unknown accessor type: " << (S32)type << LL_ENDL;
llassert(false);
return "SCALAR";
}
}
}
void Buffer::erase(Asset& asset, S32 offset, S32 length)
{
S32 idx = (S32)(this - &asset.mBuffers[0]);
mData.erase(mData.begin() + offset, mData.begin() + offset + length);
llassert(mData.size() <= size_t(INT_MAX));
mByteLength = S32(mData.size());
for (BufferView& view : asset.mBufferViews)
{
if (view.mBuffer == idx)
{
if (view.mByteOffset >= offset)
{
view.mByteOffset -= length;
}
}
}
}
bool Buffer::prep(Asset& asset)
{
if (mByteLength == 0)
{
return false;
}
LLUUID id;
if (mUri.size() == UUID_STR_SIZE && LLUUID::parseUUID(mUri, &id) && id.notNull())
{ // loaded from an asset, fetch the buffer data from the asset store
LLFileSystem file(id, LLAssetType::AT_GLTF_BIN, LLFileSystem::READ);
if (mByteLength > file.getSize())
{
LL_WARNS("GLTF") << "Unexpected glbin size: " << id << " is " << file.getSize() << " bytes, expected " << mByteLength << LL_ENDL;
return false;
}
mData.resize(mByteLength);
if (!file.read((U8*)mData.data(), mByteLength))
{
LL_WARNS("GLTF") << "Failed to load buffer data from asset: " << id << LL_ENDL;
return false;
}
}
else if (mUri.find("data:") == 0)
{ // loaded from a data URI, load the texture from the data
LL_WARNS() << "Data URIs not yet supported" << LL_ENDL;
return false;
}
else if (!asset.mFilename.empty() &&
!mUri.empty()) // <-- uri could be empty if we're loading from .glb
{
std::string dir = gDirUtilp->getDirName(asset.mFilename);
std::string bin_file = dir + gDirUtilp->getDirDelimiter() + mUri;
std::ifstream file(bin_file, std::ios::binary);
if (!file.is_open())
{
LL_WARNS("GLTF") << "Failed to open file: " << bin_file << LL_ENDL;
return false;
}
file.seekg(0, std::ios::end);
if (mByteLength > file.tellg())
{
LL_WARNS("GLTF") << "Unexpected file size: " << bin_file << " is " << file.tellg() << " bytes, expected " << mByteLength << LL_ENDL;
return false;
}
file.seekg(0, std::ios::beg);
mData.resize(mByteLength);
file.read((char*)mData.data(), mData.size());
}
// POSTCONDITION: on success, mData.size == mByteLength
llassert(mData.size() == mByteLength);
return true;
}
bool Buffer::save(Asset& asset, const std::string& folder)
{
if (mUri.substr(0, 5) == "data:")
{
LL_WARNS("GLTF") << "Data URIs not yet supported" << LL_ENDL;
return false;
}
std::string bin_file = folder + gDirUtilp->getDirDelimiter();
if (mUri.empty())
{
if (mName.empty())
{
S32 idx = (S32)(this - &asset.mBuffers[0]);
mUri = llformat("buffer_%d.bin", idx);
}
else
{
mUri = mName + ".bin";
}
}
bin_file += mUri;
std::ofstream file(bin_file, std::ios::binary);
if (!file.is_open())
{
LL_WARNS("GLTF") << "Failed to open file: " << bin_file << LL_ENDL;
return false;
}
file.write((char*)mData.data(), mData.size());
return true;
}
void Buffer::serialize(object& dst) const
{
write(mName, "name", dst);
write(mUri, "uri", dst);
write_always(mByteLength, "byteLength", dst);
};
const Buffer& Buffer::operator=(const Value& src)
{
if (src.is_object())
{
copy(src, "name", mName);
copy(src, "uri", mUri);
copy(src, "byteLength", mByteLength);
// NOTE: DO NOT attempt to handle the uri here.
// The uri is a reference to a file that is not loaded until
// after the json document is parsed
}
return *this;
}
void BufferView::serialize(object& dst) const
{
write_always(mBuffer, "buffer", dst);
write_always(mByteLength, "byteLength", dst);
write(mByteOffset, "byteOffset", dst, 0);
write(mByteStride, "byteStride", dst, 0);
write(mTarget, "target", dst, -1);
write(mName, "name", dst);
}
const BufferView& BufferView::operator=(const Value& src)
{
if (src.is_object())
{
copy(src, "buffer", mBuffer);
copy(src, "byteLength", mByteLength);
copy(src, "byteOffset", mByteOffset);
copy(src, "byteStride", mByteStride);
copy(src, "target", mTarget);
copy(src, "name", mName);
}
return *this;
}
void Accessor::serialize(object& dst) const
{
write(mName, "name", dst);
write(mBufferView, "bufferView", dst, INVALID_INDEX);
write(mByteOffset, "byteOffset", dst, 0);
write_always(mComponentType, "componentType", dst);
write_always(mCount, "count", dst);
write_always(enum_to_gltf_type(mType), "type", dst);
write(mNormalized, "normalized", dst, false);
write(mMax, "max", dst);
write(mMin, "min", dst);
}
const Accessor& Accessor::operator=(const Value& src)
{
if (src.is_object())
{
copy(src, "name", mName);
copy(src, "bufferView", mBufferView);
copy(src, "byteOffset", mByteOffset);
copy(src, "componentType", mComponentType);
copy(src, "count", mCount);
copy(src, "type", mType);
copy(src, "normalized", mNormalized);
copy(src, "max", mMax);
copy(src, "min", mMin);
}
return *this;
}
+118
View File
@@ -0,0 +1,118 @@
#pragma once
/**
* @file asset.h
* @brief LL GLTF Implementation
*
* $LicenseInfo:firstyear=2024&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2024, Linden Research, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
* $/LicenseInfo$
*/
#include "llstrider.h"
#include "boost/json.hpp"
#include "common.h"
// LL GLTF Implementation
namespace LL
{
namespace GLTF
{
class Buffer
{
public:
std::vector<U8> mData;
std::string mName;
std::string mUri;
S32 mByteLength = 0;
// erase the given range from this buffer.
// also updates all buffer views in given asset that reference this buffer
void erase(Asset& asset, S32 offset, S32 length);
bool prep(Asset& asset);
void serialize(boost::json::object& obj) const;
const Buffer& operator=(const Value& value);
bool save(Asset& asset, const std::string& folder);
};
class BufferView
{
public:
S32 mBuffer = INVALID_INDEX;
S32 mByteLength = 0;
S32 mByteOffset = 0;
S32 mByteStride = 0;
S32 mTarget = -1;
std::string mName;
void serialize(boost::json::object& obj) const;
const BufferView& operator=(const Value& value);
};
class Accessor
{
public:
enum class Type : U8
{
SCALAR,
VEC2,
VEC3,
VEC4,
MAT2,
MAT3,
MAT4
};
enum class ComponentType : U32
{
BYTE = 5120,
UNSIGNED_BYTE = 5121,
SHORT = 5122,
UNSIGNED_SHORT = 5123,
UNSIGNED_INT = 5125,
FLOAT = 5126
};
std::vector<double> mMax;
std::vector<double> mMin;
std::string mName;
S32 mBufferView = INVALID_INDEX;
S32 mByteOffset = 0;
ComponentType mComponentType = ComponentType::BYTE;
S32 mCount = 0;
Type mType = Type::SCALAR;
bool mNormalized = false;
void serialize(boost::json::object& obj) const;
const Accessor& operator=(const Value& value);
};
// convert from "SCALAR", "VEC2", etc to Accessor::Type
Accessor::Type gltf_type_to_enum(const std::string& type);
// convert from Accessor::Type to "SCALAR", "VEC2", etc
std::string enum_to_gltf_type(Accessor::Type type);
}
}
+489
View File
@@ -0,0 +1,489 @@
/**
* @file animation.cpp
* @brief LL GLTF Animation Implementation
*
* $LicenseInfo:firstyear=2024&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2024, Linden Research, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
* $/LicenseInfo$
*/
#include "../llviewerprecompiledheaders.h"
#include "asset.h"
#include "buffer_util.h"
#include "../llskinningutil.h"
using namespace LL::GLTF;
using namespace boost::json;
bool Animation::prep(Asset& asset)
{
if (!mSamplers.empty())
{
mMinTime = FLT_MAX;
mMaxTime = -FLT_MAX;
for (auto& sampler : mSamplers)
{
if (!sampler.prep(asset))
{
return false;
}
mMinTime = llmin(sampler.mMinTime, mMinTime);
mMaxTime = llmax(sampler.mMaxTime, mMaxTime);
}
}
else
{
mMinTime = mMaxTime = 0.f;
}
for (auto& channel : mRotationChannels)
{
if (!channel.prep(asset, mSamplers[channel.mSampler]))
{
return false;
}
}
for (auto& channel : mTranslationChannels)
{
if (!channel.prep(asset, mSamplers[channel.mSampler]))
{
return false;
}
}
for (auto& channel : mScaleChannels)
{
if (!channel.prep(asset, mSamplers[channel.mSampler]))
{
return false;
}
}
return true;
}
void Animation::update(Asset& asset, F32 dt)
{
mTime += dt;
apply(asset, mTime);
}
void Animation::apply(Asset& asset, float time)
{
LL_PROFILE_ZONE_SCOPED_CATEGORY_GLTF;
// convert time to animation loop time
time = fmod(time, mMaxTime - mMinTime) + mMinTime;
// apply each channel
{
LL_PROFILE_ZONE_NAMED_CATEGORY_GLTF("gltfanim - rotation");
for (auto& channel : mRotationChannels)
{
channel.apply(asset, mSamplers[channel.mSampler], time);
}
}
{
LL_PROFILE_ZONE_NAMED_CATEGORY_GLTF("gltfanim - translation");
for (auto& channel : mTranslationChannels)
{
channel.apply(asset, mSamplers[channel.mSampler], time);
}
}
{
LL_PROFILE_ZONE_NAMED_CATEGORY_GLTF("gltfanim - scale");
for (auto& channel : mScaleChannels)
{
channel.apply(asset, mSamplers[channel.mSampler], time);
}
}
};
bool Animation::Sampler::prep(Asset& asset)
{
Accessor& accessor = asset.mAccessors[mInput];
mMinTime = (F32)accessor.mMin[0];
mMaxTime = (F32)accessor.mMax[0];
mFrameTimes.resize(accessor.mCount);
LLStrider<F32> frame_times = mFrameTimes.data();
copy(asset, accessor, frame_times);
return true;
}
void Animation::Sampler::serialize(object& obj) const
{
write(mInput, "input", obj, INVALID_INDEX);
write(mOutput, "output", obj, INVALID_INDEX);
write(mInterpolation, "interpolation", obj, std::string("LINEAR"));
write(mMinTime, "min_time", obj);
write(mMaxTime, "max_time", obj);
}
const Animation::Sampler& Animation::Sampler::operator=(const Value& src)
{
if (src.is_object())
{
copy(src, "input", mInput);
copy(src, "output", mOutput);
copy(src, "interpolation", mInterpolation);
copy(src, "min_time", mMinTime);
copy(src, "max_time", mMaxTime);
}
return *this;
}
bool Animation::Channel::Target::operator==(const Channel::Target& rhs) const
{
return mNode == rhs.mNode && mPath == rhs.mPath;
}
bool Animation::Channel::Target::operator!=(const Channel::Target& rhs) const
{
return !(*this == rhs);
}
void Animation::Channel::Target::serialize(object& obj) const
{
write(mNode, "node", obj, INVALID_INDEX);
write(mPath, "path", obj);
}
const Animation::Channel::Target& Animation::Channel::Target::operator=(const Value& src)
{
if (src.is_object())
{
copy(src, "node", mNode);
copy(src, "path", mPath);
}
return *this;
}
void Animation::Channel::serialize(object& obj) const
{
write(mSampler, "sampler", obj, INVALID_INDEX);
write(mTarget, "target", obj);
}
const Animation::Channel& Animation::Channel::operator=(const Value& src)
{
if (src.is_object())
{
copy(src, "sampler", mSampler);
copy(src, "target", mTarget);
}
return *this;
}
void Animation::Sampler::getFrameInfo(Asset& asset, F32 time, U32& frameIndex, F32& t)
{
LL_PROFILE_ZONE_SCOPED_CATEGORY_GLTF;
llassert(mFrameTimes.size() > 1); // if there is only one frame, there is no need to interpolate
if (time < mMinTime)
{
frameIndex = 0;
t = 0.0f;
return;
}
frameIndex = U32(mFrameTimes.size()) - 2;
t = 1.f;
if (time > mMaxTime)
{
return;
}
if (time < mLastFrameTime)
{
mLastFrameIndex = 0;
}
mLastFrameTime = time;
U32 idx = mLastFrameIndex;
for (U32 i = idx; i < (U32)mFrameTimes.size() - 1; i++)
{
if (time >= mFrameTimes[i] && time < mFrameTimes[i + 1])
{
frameIndex = i;
t = (time - mFrameTimes[i]) / (mFrameTimes[i + 1] - mFrameTimes[i]);
mLastFrameIndex = frameIndex;
return;
}
}
}
bool Animation::RotationChannel::prep(Asset& asset, Animation::Sampler& sampler)
{
Accessor& accessor = asset.mAccessors[sampler.mOutput];
copy(asset, accessor, mRotations);
return true;
}
void Animation::RotationChannel::apply(Asset& asset, Sampler& sampler, F32 time)
{
U32 frameIndex;
F32 t;
Node& node = asset.mNodes[mTarget.mNode];
if (sampler.mFrameTimes.size() < 2)
{
node.setRotation(mRotations[0]);
}
else
{
sampler.getFrameInfo(asset, time, frameIndex, t);
// interpolate
quat qf = glm::slerp(mRotations[frameIndex], mRotations[frameIndex + 1], t);
qf = glm::normalize(qf);
node.setRotation(qf);
}
}
bool Animation::TranslationChannel::prep(Asset& asset, Animation::Sampler& sampler)
{
Accessor& accessor = asset.mAccessors[sampler.mOutput];
copy(asset, accessor, mTranslations);
return true;
}
void Animation::TranslationChannel::apply(Asset& asset, Sampler& sampler, F32 time)
{
U32 frameIndex;
F32 t;
Node& node = asset.mNodes[mTarget.mNode];
if (sampler.mFrameTimes.size() < 2)
{
node.setTranslation(mTranslations[0]);
}
else
{
sampler.getFrameInfo(asset, time, frameIndex, t);
// interpolate
const vec3& v0 = mTranslations[frameIndex];
const vec3& v1 = mTranslations[frameIndex + 1];
vec3 vf = v0 + t * (v1 - v0);
node.setTranslation(vf);
}
}
bool Animation::ScaleChannel::prep(Asset& asset, Animation::Sampler& sampler)
{
Accessor& accessor = asset.mAccessors[sampler.mOutput];
copy(asset, accessor, mScales);
return true;
}
void Animation::ScaleChannel::apply(Asset& asset, Sampler& sampler, F32 time)
{
U32 frameIndex;
F32 t;
Node& node = asset.mNodes[mTarget.mNode];
if (sampler.mFrameTimes.size() < 2)
{
node.setScale(mScales[0]);
}
else
{
sampler.getFrameInfo(asset, time, frameIndex, t);
// interpolate
const vec3& v0 = mScales[frameIndex];
const vec3& v1 = mScales[frameIndex + 1];
vec3 vf = v0 + t * (v1 - v0);
node.setScale(vf);
}
}
void Animation::serialize(object& obj) const
{
write(mName, "name", obj);
write(mSamplers, "samplers", obj);
std::vector<Channel> channels;
channels.insert(channels.end(), mRotationChannels.begin(), mRotationChannels.end());
channels.insert(channels.end(), mTranslationChannels.begin(), mTranslationChannels.end());
channels.insert(channels.end(), mScaleChannels.begin(), mScaleChannels.end());
write(channels, "channels", obj);
}
const Animation& Animation::operator=(const Value& src)
{
if (src.is_object())
{
const object& obj = src.as_object();
copy(obj, "name", mName);
copy(obj, "samplers", mSamplers);
// make a temporory copy of generic channels
std::vector<Channel> channels;
copy(obj, "channels", channels);
// break up into channel specific implementations
for (auto& channel: channels)
{
if (channel.mTarget.mPath == "rotation")
{
mRotationChannels.push_back(channel);
}
else if (channel.mTarget.mPath == "translation")
{
mTranslationChannels.push_back(channel);
}
else if (channel.mTarget.mPath == "scale")
{
mScaleChannels.push_back(channel);
}
}
}
return *this;
}
Skin::~Skin()
{
if (mUBO)
{
glDeleteBuffers(1, &mUBO);
}
}
void Skin::uploadMatrixPalette(Asset& asset)
{
// prepare matrix palette
LL_PROFILE_ZONE_SCOPED_CATEGORY_GLTF;
U32 max_joints = LLSkinningUtil::getMaxGLTFJointCount();
if (mUBO == 0)
{
glGenBuffers(1, &mUBO);
}
size_t joint_count = llmin<size_t>(max_joints, mJoints.size());
std::vector<mat4> t_mp;
t_mp.resize(joint_count);
for (U32 i = 0; i < joint_count; ++i)
{
Node& joint = asset.mNodes[mJoints[i]];
// build matrix palette in asset space
t_mp[i] = joint.mAssetMatrix * mInverseBindMatricesData[i];
}
std::vector<F32> glmp;
glmp.resize(joint_count * 12);
F32* mp = glmp.data();
for (U32 i = 0; i < joint_count; ++i)
{
F32* m = glm::value_ptr(t_mp[i]);
U32 idx = i * 12;
mp[idx + 0] = m[0];
mp[idx + 1] = m[1];
mp[idx + 2] = m[2];
mp[idx + 3] = m[12];
mp[idx + 4] = m[4];
mp[idx + 5] = m[5];
mp[idx + 6] = m[6];
mp[idx + 7] = m[13];
mp[idx + 8] = m[8];
mp[idx + 9] = m[9];
mp[idx + 10] = m[10];
mp[idx + 11] = m[14];
}
glBindBuffer(GL_UNIFORM_BUFFER, mUBO);
glBufferData(GL_UNIFORM_BUFFER, glmp.size() * sizeof(F32), glmp.data(), GL_STREAM_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
}
bool Skin::prep(Asset& asset)
{
if (mInverseBindMatrices != INVALID_INDEX)
{
Accessor& accessor = asset.mAccessors[mInverseBindMatrices];
copy(asset, accessor, mInverseBindMatricesData);
}
return true;
}
const Skin& Skin::operator=(const Value& src)
{
if (src.is_object())
{
copy(src, "name", mName);
copy(src, "skeleton", mSkeleton);
copy(src, "inverseBindMatrices", mInverseBindMatrices);
copy(src, "joints", mJoints);
}
return *this;
}
void Skin::serialize(object& obj) const
{
write(mInverseBindMatrices, "inverseBindMatrices", obj, INVALID_INDEX);
write(mJoints, "joints", obj);
write(mName, "name", obj);
write(mSkeleton, "skeleton", obj, INVALID_INDEX);
}
+165
View File
@@ -0,0 +1,165 @@
#pragma once
/**
* @file animation.h
* @brief LL GLTF Animation Implementation
*
* $LicenseInfo:firstyear=2024&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2024, Linden Research, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
* $/LicenseInfo$
*/
#include "accessor.h"
// LL GLTF Implementation
namespace LL
{
namespace GLTF
{
class Asset;
class Animation
{
public:
class Sampler
{
public:
std::vector<F32> mFrameTimes;
F32 mMinTime = -FLT_MAX;
F32 mMaxTime = FLT_MAX;
S32 mInput = INVALID_INDEX;
S32 mOutput = INVALID_INDEX;
std::string mInterpolation;
F32 mLastFrameTime = 0.f;
U32 mLastFrameIndex = 0;
bool prep(Asset& asset);
void serialize(boost::json::object& dst) const;
const Sampler& operator=(const Value& value);
// get the frame index and time for the specified time
// asset -- the asset to reference for Accessors
// time -- the animation time to get the frame info for
// frameIndex -- index of the closest frame that precedes the specified time
// t - interpolant value between the frameIndex and the next frame
void getFrameInfo(Asset& asset, F32 time, U32& frameIndex, F32& t);
};
class Channel
{
public:
class Target
{
public:
S32 mNode = INVALID_INDEX;
std::string mPath;
bool operator==(const Target& other) const;
bool operator!=(const Target& other) const;
void serialize(boost::json::object& dst) const;
const Target& operator=(const Value& value);
};
S32 mSampler = INVALID_INDEX;
Target mTarget;
void serialize(boost::json::object& dst) const;
const Channel& operator=(const Value& value);
};
class RotationChannel : public Channel
{
public:
RotationChannel() = default;
RotationChannel(const Channel& channel) : Channel(channel) {}
std::vector<quat> mRotations;
// prepare data needed for rendering
// asset -- asset to reference for Accessors
// sampler -- Sampler associated with this channel
bool prep(Asset& asset, Sampler& sampler);
void apply(Asset& asset, Sampler& sampler, F32 time);
};
class TranslationChannel : public Channel
{
public:
TranslationChannel() = default;
TranslationChannel(const Channel& channel) : Channel(channel) {}
std::vector<vec3> mTranslations;
// prepare data needed for rendering
// asset -- asset to reference for Accessors
// sampler -- Sampler associated with this channel
bool prep(Asset& asset, Sampler& sampler);
void apply(Asset& asset, Sampler& sampler, F32 time);
};
class ScaleChannel : public Channel
{
public:
ScaleChannel() = default;
ScaleChannel(const Channel& channel) : Channel(channel) {}
std::vector<vec3> mScales;
// prepare data needed for rendering
// asset -- asset to reference for Accessors
// sampler -- Sampler associated with this channel
bool prep(Asset& asset, Sampler& sampler);
void apply(Asset& asset, Sampler& sampler, F32 time);
};
std::string mName;
std::vector<Sampler> mSamplers;
// min/max time values for all samplers combined
F32 mMinTime = 0.f;
F32 mMaxTime = 0.f;
// current time of the animation
F32 mTime = 0.f;
std::vector<RotationChannel> mRotationChannels;
std::vector<TranslationChannel> mTranslationChannels;
std::vector<ScaleChannel> mScaleChannels;
void serialize(boost::json::object& dst) const;
const Animation& operator=(const Value& value);
bool prep(Asset& asset);
void update(Asset& asset, float dt);
// apply this animation at the specified time
void apply(Asset& asset, F32 time);
};
}
}
File diff suppressed because it is too large Load Diff
+457
View File
@@ -0,0 +1,457 @@
#pragma once
/**
* @file asset.h
* @brief LL GLTF Implementation
*
* $LicenseInfo:firstyear=2024&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2024, Linden Research, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
* $/LicenseInfo$
*/
#include "llvertexbuffer.h"
#include "llvolumeoctree.h"
#include "accessor.h"
#include "primitive.h"
#include "animation.h"
#include "boost/json.hpp"
#include "common.h"
#include "../llviewertexture.h"
#include "llglslshader.h"
extern F32SecondsImplicit gFrameTimeSeconds;
// wingdi defines OPAQUE, which conflicts with our enum
#if defined(OPAQUE)
#undef OPAQUE
#endif
// LL GLTF Implementation
namespace LL
{
namespace GLTF
{
class Asset;
class Extension
{
public:
// true if this extension is present in the gltf file
// otherwise false
bool mPresent = false;
};
class TextureTransform : public Extension // KHR_texture_transform implementation
{
public:
vec2 mOffset = vec2(0.f, 0.f);
F32 mRotation = 0.f;
vec2 mScale = vec2(1.f, 1.f);
S32 mTexCoord = INVALID_INDEX;
// get the texture transform as a packed array of vec4's
// dst MUST point to at least 2 vec4's
void getPacked(vec4* dst) const;
const TextureTransform& operator=(const Value& src);
void serialize(boost::json::object& dst) const;
};
class TextureInfo
{
public:
S32 mIndex = INVALID_INDEX;
S32 mTexCoord = 0;
TextureTransform mTextureTransform;
bool operator==(const TextureInfo& rhs) const;
bool operator!=(const TextureInfo& rhs) const;
// get the UV channel that should be used for sampling this texture
// returns mTextureTransform.mTexCoord if present and valid, otherwise mTexCoord
S32 getTexCoord() const;
const TextureInfo& operator=(const Value& src);
void serialize(boost::json::object& dst) const;
};
class NormalTextureInfo : public TextureInfo
{
public:
F32 mScale = 1.0f;
const NormalTextureInfo& operator=(const Value& src);
void serialize(boost::json::object& dst) const;
};
class OcclusionTextureInfo : public TextureInfo
{
public:
F32 mStrength = 1.0f;
const OcclusionTextureInfo& operator=(const Value& src);
void serialize(boost::json::object& dst) const;
};
class Material
{
public:
class Unlit : public Extension // KHR_materials_unlit implementation
{
public:
const Unlit& operator=(const Value& src);
void serialize(boost::json::object& dst) const;
};
enum class AlphaMode
{
OPAQUE,
MASK,
BLEND
};
class PbrMetallicRoughness
{
public:
vec4 mBaseColorFactor = vec4(1.f,1.f,1.f,1.f);
TextureInfo mBaseColorTexture;
F32 mMetallicFactor = 1.0f;
F32 mRoughnessFactor = 1.0f;
TextureInfo mMetallicRoughnessTexture;
bool operator==(const PbrMetallicRoughness& rhs) const;
bool operator!=(const PbrMetallicRoughness& rhs) const;
const PbrMetallicRoughness& operator=(const Value& src);
void serialize(boost::json::object& dst) const;
};
PbrMetallicRoughness mPbrMetallicRoughness;
NormalTextureInfo mNormalTexture;
OcclusionTextureInfo mOcclusionTexture;
TextureInfo mEmissiveTexture;
std::string mName;
vec3 mEmissiveFactor = vec3(0.f, 0.f, 0.f);
AlphaMode mAlphaMode = AlphaMode::OPAQUE;
F32 mAlphaCutoff = 0.5f;
bool mDoubleSided = false;
Unlit mUnlit;
bool isMultiUV() const;
const Material& operator=(const Value& src);
void serialize(boost::json::object& dst) const;
};
class Mesh
{
public:
std::vector<Primitive> mPrimitives;
std::vector<double> mWeights;
std::string mName;
const Mesh& operator=(const Value& src);
void serialize(boost::json::object& dst) const;
bool prep(Asset& asset);
};
class Node
{
public:
mat4 mMatrix = glm::identity<mat4>(); //local transform
mat4 mAssetMatrix; //transform from local to asset space
mat4 mAssetMatrixInv; //transform from asset to local space
vec3 mTranslation = vec3(0,0,0);
quat mRotation = glm::identity<quat>();
vec3 mScale = vec3(1.f,1.f,1.f);
// if true, mMatrix is valid and up to date
bool mMatrixValid = false;
// if true, translation/rotation/scale are valid and up to date
bool mTRSValid = false;
bool mNeedsApplyMatrix = false;
std::vector<S32> mChildren;
S32 mParent = INVALID_INDEX;
S32 mMesh = INVALID_INDEX;
S32 mSkin = INVALID_INDEX;
std::string mName;
const Node& operator=(const Value& src);
void serialize(boost::json::object& dst) const;
// update mAssetMatrix and mAssetMatrixInv
void updateTransforms(Asset& asset, const mat4& parentMatrix);
// ensure mMatrix is valid -- if mMatrixValid is false and mTRSValid is true, will update mMatrix to match Translation/Rotation/Scale
void makeMatrixValid();
// ensure Translation/Rotation/Scale are valid -- if mTRSValid is false and mMatrixValid is true, will update Translation/Rotation/Scale to match mMatrix
void makeTRSValid();
// Set rotation of this node
// SIDE EFFECT: invalidates mMatrix
void setRotation(const quat& rotation);
// Set translation of this node
// SIDE EFFECT: invalidates mMatrix
void setTranslation(const vec3& translation);
// Set scale of this node
// SIDE EFFECT: invalidates mMatrix
void setScale(const vec3& scale);
};
class Skin
{
public:
~Skin();
S32 mInverseBindMatrices = INVALID_INDEX;
S32 mSkeleton = INVALID_INDEX;
U32 mUBO = 0;
std::vector<S32> mJoints;
std::string mName;
std::vector<mat4> mInverseBindMatricesData;
bool prep(Asset& asset);
void uploadMatrixPalette(Asset& asset);
const Skin& operator=(const Value& src);
void serialize(boost::json::object& dst) const;
};
class Scene
{
public:
std::vector<S32> mNodes;
std::string mName;
const Scene& operator=(const Value& src);
void serialize(boost::json::object& dst) const;
void updateTransforms(Asset& asset);
void updateRenderTransforms(Asset& asset, const mat4& modelview);
};
class Texture
{
public:
S32 mSampler = INVALID_INDEX;
S32 mSource = INVALID_INDEX;
std::string mName;
const Texture& operator=(const Value& src);
void serialize(boost::json::object& dst) const;
};
class Sampler
{
public:
S32 mMagFilter = LINEAR;
S32 mMinFilter = LINEAR_MIPMAP_LINEAR;
S32 mWrapS = REPEAT;
S32 mWrapT = REPEAT;
std::string mName;
const Sampler& operator=(const Value& src);
void serialize(boost::json::object& dst) const;
};
class Image
{
public:
std::string mName;
std::string mUri;
std::string mMimeType;
S32 mBufferView = INVALID_INDEX;
S32 mWidth = -1;
S32 mHeight = -1;
S32 mComponent = -1;
S32 mBits = -1;
S32 mPixelType = -1;
LLPointer<LLViewerFetchedTexture> mTexture;
const Image& operator=(const Value& src);
void serialize(boost::json::object& dst) const;
// save image to disk
// may remove image data from bufferviews and convert to
// file uri if necessary
bool save(Asset& asset, const std::string& filename);
// erase the buffer view associated with this image
// free any associated GLTF resources
// preserve only uri and name
void clearData(Asset& asset);
bool prep(Asset& asset);
};
// Render Batch -- vertex buffer and list of primitives to render using
// said vertex buffer
class RenderBatch
{
public:
struct PrimitiveData
{
S32 mPrimitiveIndex = INVALID_INDEX;
S32 mNodeIndex = INVALID_INDEX;
};
LLPointer<LLVertexBuffer> mVertexBuffer;
std::vector<PrimitiveData> mPrimitives;
};
class RenderData
{
public:
// list of render batches
// indexed by [material index + 1](0 is reserved for default material)
// there should be exactly one render batch per material per variant
std::vector<RenderBatch> mBatches[LLGLSLShader::NUM_GLTF_VARIANTS];
};
// C++ representation of a GLTF Asset
class Asset
{
public:
static const std::string minVersion_default;
std::vector<Scene> mScenes;
std::vector<Node> mNodes;
std::vector<Mesh> mMeshes;
std::vector<Material> mMaterials;
std::vector<Buffer> mBuffers;
std::vector<BufferView> mBufferViews;
std::vector<Texture> mTextures;
std::vector<Sampler> mSamplers;
std::vector<Image> mImages;
std::vector<Accessor> mAccessors;
std::vector<Animation> mAnimations;
std::vector<Skin> mSkins;
std::vector<std::string> mExtensionsUsed;
std::vector<std::string> mExtensionsRequired;
std::string mVersion;
std::string mGenerator;
std::string mMinVersion;
std::string mCopyright;
S32 mScene = INVALID_INDEX;
Value mExtras;
U32 mPendingBuffers = 0;
// local file this asset was loaded from (if any)
std::string mFilename;
// the last time update() was called according to gFrameTimeSeconds
F32 mLastUpdateTime = gFrameTimeSeconds;
// data used for rendering
// 0 - single sided
// 1 - double sided
RenderData mRenderData[2];
// UBO for storing node transforms
U32 mNodesUBO = 0;
// UBO for storing material data
U32 mMaterialsUBO = 0;
// prepare for first time use
bool prep();
// Called periodically (typically once per frame)
// Any ongoing work (such as animations) should be handled here
// NOT guaranteed to be called every frame
// MAY be called more than once per frame
// Upon return, all Node Matrix transforms should be up to date
void update();
// update asset-to-node and node-to-asset transforms
void updateTransforms();
// upload matrices to UBO
void uploadTransforms();
// upload materils to UBO
void uploadMaterials();
// return the index of the node that the line segment intersects with, or -1 if no hit
// input and output values must be in this asset's local coordinate frame
S32 lineSegmentIntersect(const LLVector4a& start, const LLVector4a& end,
LLVector4a* intersection = nullptr, // return the intersection point
LLVector2* tex_coord = nullptr, // return the texture coordinates of the intersection point
LLVector4a* normal = nullptr, // return the surface normal at the intersection point
LLVector4a* tangent = nullptr, // return the surface tangent at the intersection point
S32* primitive_hitp = nullptr // return the index of the primitive that was hit
);
Asset() = default;
Asset(const Value& src);
// load from given file
// accepts .gltf and .glb files
// Any existing data will be lost
// returns result of prep() on success
bool load(std::string_view filename);
// load .glb contents from memory
// data - binary contents of .glb file
// returns result of prep() on success
bool loadBinary(const std::string& data);
const Asset& operator=(const Value& src);
void serialize(boost::json::object& dst) const;
// save the asset to the given .gltf file
// saves images and bins alongside the gltf file
bool save(const std::string& filename);
// remove the bufferview at the given index
// updates all bufferview indices in this Asset as needed
void eraseBufferView(S32 bufferView);
// return true if this Asset has been loaded as a local preview
// Local previews may be uploaded or exported to disk
bool isLocalPreview() { return !mFilename.empty(); }
};
Material::AlphaMode gltf_alpha_mode_to_enum(const std::string& alpha_mode);
std::string enum_to_gltf_alpha_mode(Material::AlphaMode alpha_mode);
}
}
File diff suppressed because it is too large Load Diff
+97
View File
@@ -0,0 +1,97 @@
#pragma once
/**
* @file common.h
* @brief LL GLTF Implementation
*
* $LicenseInfo:firstyear=2024&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2024, Linden Research, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
* $/LicenseInfo$
*/
#include "glm/vec2.hpp"
#include "glm/vec3.hpp"
#include "glm/vec4.hpp"
#include "glm/mat4x4.hpp"
#include "glm/gtc/type_ptr.hpp"
#include "glm/ext/quaternion_float.hpp"
#include "glm/gtx/quaternion.hpp"
#include "glm/gtx/matrix_decompose.hpp"
#include <boost/json.hpp>
// Common types and constants used in the GLTF implementation
namespace LL
{
namespace GLTF
{
constexpr S32 INVALID_INDEX = -1;
using Value = boost::json::value;
using mat4 = glm::mat4;
using vec4 = glm::vec4;
using vec3 = glm::vec3;
using vec2 = glm::vec2;
using quat = glm::quat;
constexpr S32 LINEAR = 9729;
constexpr S32 NEAREST = 9728;
constexpr S32 NEAREST_MIPMAP_NEAREST = 9984;
constexpr S32 LINEAR_MIPMAP_NEAREST = 9985;
constexpr S32 NEAREST_MIPMAP_LINEAR = 9986;
constexpr S32 LINEAR_MIPMAP_LINEAR = 9987;
constexpr S32 CLAMP_TO_EDGE = 33071;
constexpr S32 MIRRORED_REPEAT = 33648;
constexpr S32 REPEAT = 10497;
class Asset;
class Material;
class TextureInfo;
class NormalTextureInfo;
class OcclusionTextureInfo;
class Mesh;
class Node;
class Scene;
class Texture;
class Sampler;
class Image;
class Animation;
class Skin;
class Camera;
class Light;
class Primitive;
class Accessor;
class BufferView;
class Buffer;
enum class TextureType : U8
{
BASE_COLOR = 0,
NORMAL,
METALLIC_ROUGHNESS,
OCCLUSION,
EMISSIVE
};
constexpr U32 TEXTURE_TYPE_COUNT = 5;
}
}
+819
View File
@@ -0,0 +1,819 @@
/**
* @file primitive.cpp
* @brief LL GLTF Implementation
*
* $LicenseInfo:firstyear=2024&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2024, Linden Research, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
* $/LicenseInfo$
*/
#include "../llviewerprecompiledheaders.h"
#include "asset.h"
#include "buffer_util.h"
#include "../llviewershadermgr.h"
#include "mikktspace/mikktspace.hh"
#include "meshoptimizer/meshoptimizer.h"
using namespace LL::GLTF;
using namespace boost::json;
// Mesh data useful for Mikktspace tangent generation (and flat normal generation)
struct MikktMesh
{
std::vector<LLVector3> p; //positions
std::vector<LLVector3> n; //normals
std::vector<LLVector4> t; //tangents
std::vector<LLVector2> tc0; //texcoords 0
std::vector<LLVector2> tc1; //texcoords 1
std::vector<LLColor4U> c; //colors
std::vector<LLVector4> w; //weights
std::vector<U64> j; //joints
// initialize from src primitive and make an unrolled triangle list
// returns false if the Primitive cannot be converted to a triangle list
bool copy(const Primitive* prim)
{
bool indexed = !prim->mIndexArray.empty();
size_t vert_count = indexed ? prim->mIndexArray.size() : prim->mPositions.size();
size_t triangle_count = 0;
if (prim->mMode == Primitive::Mode::TRIANGLE_STRIP ||
prim->mMode == Primitive::Mode::TRIANGLE_FAN)
{
triangle_count = vert_count - 2;
}
else if (prim->mMode == Primitive::Mode::TRIANGLES)
{
triangle_count = vert_count / 3;
}
else
{
LL_WARNS("GLTF") << "Unsupported primitive mode for conversion to triangles: " << (S32)prim->mMode << LL_ENDL;
return false;
}
vert_count = triangle_count * 3;
llassert(vert_count <= size_t(U32_MAX)); // triangle_count will also naturally be under the limit
p.resize(vert_count);
n.resize(vert_count);
tc0.resize(vert_count);
c.resize(vert_count);
bool has_normals = !prim->mNormals.empty();
if (has_normals)
{
n.resize(vert_count);
}
bool has_tangents = !prim->mTangents.empty();
if (has_tangents)
{
t.resize(vert_count);
}
bool rigged = !prim->mWeights.empty();
if (rigged)
{
w.resize(vert_count);
j.resize(vert_count);
}
bool multi_uv = !prim->mTexCoords1.empty();
if (multi_uv)
{
tc1.resize(vert_count);
}
for (U32 tri_idx = 0; tri_idx < U32(triangle_count); ++tri_idx)
{
U32 idx[3] = {0, 0, 0};
if (prim->mMode == Primitive::Mode::TRIANGLES)
{
idx[0] = tri_idx * 3;
idx[1] = tri_idx * 3 + 1;
idx[2] = tri_idx * 3 + 2;
}
else if (prim->mMode == Primitive::Mode::TRIANGLE_STRIP)
{
idx[0] = tri_idx;
idx[1] = tri_idx + 1;
idx[2] = tri_idx + 2;
if (tri_idx % 2 != 0)
{
std::swap(idx[1], idx[2]);
}
}
else if (prim->mMode == Primitive::Mode::TRIANGLE_FAN)
{
idx[0] = 0;
idx[1] = tri_idx + 1;
idx[2] = tri_idx + 2;
}
// <FS:Beq> unknown mode leaves idx uninitialised
else
{
LL_WARNS("GLTF") << "Unsupported primitive mode for conversion to triangles: " << (S32) prim->mMode << LL_ENDL;
return false;
}
// </FS:Beq>
if (indexed)
{
idx[0] = prim->mIndexArray[idx[0]];
idx[1] = prim->mIndexArray[idx[1]];
idx[2] = prim->mIndexArray[idx[2]];
}
for (U32 v = 0; v < 3; ++v)
{
U32 i = tri_idx * 3 + v;
p[i].set(prim->mPositions[idx[v]].getF32ptr());
tc0[i].set(prim->mTexCoords0[idx[v]]);
c[i] = prim->mColors[idx[v]];
if (multi_uv)
{
tc1[i].set(prim->mTexCoords1[idx[v]]);
}
if (has_normals)
{
n[i].set(prim->mNormals[idx[v]].getF32ptr());
}
if (rigged)
{
w[i].set(prim->mWeights[idx[v]].getF32ptr());
j[i] = prim->mJoints[idx[v]];
}
}
}
return true;
}
void genNormals()
{
size_t tri_count = p.size() / 3;
for (size_t i = 0; i < tri_count; ++i)
{
LLVector3 v0 = p[i * 3];
LLVector3 v1 = p[i * 3 + 1];
LLVector3 v2 = p[i * 3 + 2];
LLVector3 normal = (v1 - v0) % (v2 - v0);
normal.normalize();
n[i * 3] = normal;
n[i * 3 + 1] = normal;
n[i * 3 + 2] = normal;
}
}
void genTangents()
{
t.resize(p.size());
mikk::Mikktspace ctx(*this);
ctx.genTangSpace();
}
// write to target primitive as an indexed triangle list
// Only modifies runtime data, does not modify the original GLTF data
void write(Primitive* prim) const
{
//re-weld
std::vector<meshopt_Stream> mos =
{
{ &p[0], sizeof(LLVector3), sizeof(LLVector3) },
{ &n[0], sizeof(LLVector3), sizeof(LLVector3) },
{ &t[0], sizeof(LLVector4), sizeof(LLVector4) },
{ &tc0[0], sizeof(LLVector2), sizeof(LLVector2) },
{ &c[0], sizeof(LLColor4U), sizeof(LLColor4U) }
};
if (!w.empty())
{
mos.push_back({ &w[0], sizeof(LLVector4), sizeof(LLVector4) });
mos.push_back({ &j[0], sizeof(U64), sizeof(U64) });
}
if (!tc1.empty())
{
mos.push_back({ &tc1[0], sizeof(LLVector2), sizeof(LLVector2) });
}
std::vector<U32> remap;
remap.resize(p.size());
size_t stream_count = mos.size();
size_t vert_count = meshopt_generateVertexRemapMulti(&remap[0], nullptr, p.size(), p.size(), mos.data(), stream_count);
prim->mTexCoords0.resize(vert_count);
prim->mNormals.resize(vert_count);
prim->mTangents.resize(vert_count);
prim->mPositions.resize(vert_count);
prim->mColors.resize(vert_count);
if (!w.empty())
{
prim->mWeights.resize(vert_count);
prim->mJoints.resize(vert_count);
}
if (!tc1.empty())
{
prim->mTexCoords1.resize(vert_count);
}
prim->mIndexArray.resize(remap.size());
for (int i = 0; i < remap.size(); ++i)
{
U32 src_idx = i;
U32 dst_idx = remap[i];
prim->mIndexArray[i] = dst_idx;
prim->mPositions[dst_idx].load3(p[src_idx].mV);
prim->mNormals[dst_idx].load3(n[src_idx].mV);
prim->mTexCoords0[dst_idx] = tc0[src_idx];
prim->mTangents[dst_idx].loadua(t[src_idx].mV);
prim->mColors[dst_idx] = c[src_idx];
if (!w.empty())
{
prim->mWeights[dst_idx].loadua(w[src_idx].mV);
prim->mJoints[dst_idx] = j[src_idx];
}
if (!tc1.empty())
{
prim->mTexCoords1[dst_idx] = tc1[src_idx];
}
}
prim->mGLMode = LLRender::TRIANGLES;
}
uint32_t GetNumFaces()
{
return uint32_t(p.size()/3);
}
uint32_t GetNumVerticesOfFace(const uint32_t face_num)
{
return 3;
}
mikk::float3 GetPosition(const uint32_t face_num, const uint32_t vert_num)
{
F32* v = p[face_num * 3 + vert_num].mV;
return mikk::float3(v);
}
mikk::float3 GetTexCoord(const uint32_t face_num, const uint32_t vert_num)
{
F32* uv = tc0[face_num * 3 + vert_num].mV;
return mikk::float3(uv[0], 1.f-uv[1], 1.0f);
}
mikk::float3 GetNormal(const uint32_t face_num, const uint32_t vert_num)
{
F32* normal = n[face_num * 3 + vert_num].mV;
return mikk::float3(normal);
}
void SetTangentSpace(const uint32_t face_num, const uint32_t vert_num, mikk::float3 T, bool orientation)
{
S32 i = face_num * 3 + vert_num;
t[i].set(T.x, T.y, T.z, orientation ? 1.0f : -1.0f);
}
};
static void vertical_flip(std::vector<LLVector2>& texcoords)
{
for (auto& tc : texcoords)
{
tc[1] = 1.f - tc[1];
}
}
bool Primitive::prep(Asset& asset)
{
// allocate vertex buffer
// We diverge from the intent of the GLTF format here to work with our existing render pipeline
// GLTF wants us to copy the buffer views into GPU storage as is and build render commands that source that data.
// For our engine, though, it's better to rearrange the buffers at load time into a layout that's more consistent.
// The GLTF native approach undoubtedly works well if you can count on VAOs, but VAOs perform much worse with our scenes.
// load vertex data
for (auto& it : mAttributes)
{
const std::string& attribName = it.first;
Accessor& accessor = asset.mAccessors[it.second];
// load vertex data
if (attribName == "POSITION")
{
copy(asset, accessor, mPositions);
}
else if (attribName == "NORMAL")
{
copy(asset, accessor, mNormals);
}
else if (attribName == "TANGENT")
{
copy(asset, accessor, mTangents);
}
else if (attribName == "COLOR_0")
{
copy(asset, accessor, mColors);
}
else if (attribName == "TEXCOORD_0")
{
copy(asset, accessor, mTexCoords0);
}
else if (attribName == "TEXCOORD_1")
{
copy(asset, accessor, mTexCoords1);
}
else if (attribName == "JOINTS_0")
{
copy(asset, accessor, mJoints);
}
else if (attribName == "WEIGHTS_0")
{
copy(asset, accessor, mWeights);
}
}
// copy index buffer
if (mIndices != INVALID_INDEX)
{
Accessor& accessor = asset.mAccessors[mIndices];
copy(asset, accessor, mIndexArray);
for (auto& idx : mIndexArray)
{
if (idx >= mPositions.size())
{
LL_WARNS("GLTF") << "Invalid index array" << LL_ENDL;
return false;
}
}
}
else
{ //everything must be indexed at runtime
mIndexArray.resize(mPositions.size());
for (U32 i = 0; i < mPositions.size(); ++i)
{
mIndexArray[i] = i;
}
}
U32 mask = LLVertexBuffer::MAP_VERTEX;
mShaderVariant = 0;
if (!mWeights.empty())
{
mShaderVariant |= LLGLSLShader::GLTFVariant::RIGGED;
mask |= LLVertexBuffer::MAP_WEIGHT4;
mask |= LLVertexBuffer::MAP_JOINT;
}
if (mTexCoords0.empty())
{
mTexCoords0.resize(mPositions.size());
}
mask |= LLVertexBuffer::MAP_TEXCOORD0;
if (!mTexCoords1.empty())
{
mask |= LLVertexBuffer::MAP_TEXCOORD1;
}
if (mColors.empty())
{
mColors.resize(mPositions.size(), LLColor4U::white);
}
mask |= LLVertexBuffer::MAP_COLOR;
bool unlit = false;
// bake material basecolor into color array
if (mMaterial != INVALID_INDEX)
{
const Material& material = asset.mMaterials[mMaterial];
LLColor4 baseColor(glm::value_ptr(material.mPbrMetallicRoughness.mBaseColorFactor));
for (auto& dst : mColors)
{
dst = LLColor4U(baseColor * LLColor4(dst));
}
if (material.mUnlit.mPresent)
{ // material uses KHR_materials_unlit
mShaderVariant |= LLGLSLShader::GLTFVariant::UNLIT;
unlit = true;
}
if (material.isMultiUV())
{
mShaderVariant |= LLGLSLShader::GLTFVariant::MULTI_UV;
}
}
if (mNormals.empty() && !unlit)
{
mTangents.clear();
if (mMode == Mode::POINTS || mMode == Mode::LINES || mMode == Mode::LINE_LOOP || mMode == Mode::LINE_STRIP)
{ //no normals and no surfaces, this primitive is unlit
mTangents.clear();
mShaderVariant |= LLGLSLShader::GLTFVariant::UNLIT;
unlit = true;
}
else
{
// unroll into non-indexed array of flat shaded triangles
MikktMesh data;
if (!data.copy(this))
{
return false;
}
data.genNormals();
data.genTangents();
data.write(this);
}
}
if (mTangents.empty() && !unlit)
{ // NOTE: must be done last because tangent generation rewrites the other arrays
// adapted from usage of Mikktspace in llvolume.cpp
if (mMode == Mode::POINTS || mMode == Mode::LINES || mMode == Mode::LINE_LOOP || mMode == Mode::LINE_STRIP)
{
// for points and lines, just make sure tangent is perpendicular to normal
mTangents.resize(mNormals.size());
LLVector4a up(0.f, 0.f, 1.f, 0.f);
LLVector4a left(1.f, 0.f, 0.f, 0.f);
for (U32 i = 0; i < mNormals.size(); ++i)
{
if (fabsf(mNormals[i].getF32ptr()[2]) < 0.999f)
{
mTangents[i] = up.cross3(mNormals[i]);
}
else
{
mTangents[i] = left.cross3(mNormals[i]);
}
mTangents[i].getF32ptr()[3] = 1.f;
}
}
else
{
MikktMesh data;
if (!data.copy(this))
{
return false;
}
data.genTangents();
data.write(this);
}
}
if (!mNormals.empty())
{
mask |= LLVertexBuffer::MAP_NORMAL;
}
if (!mTangents.empty())
{
mask |= LLVertexBuffer::MAP_TANGENT;
}
mAttributeMask = mask;
if (mMaterial != INVALID_INDEX)
{
Material& material = asset.mMaterials[mMaterial];
if (material.mAlphaMode == Material::AlphaMode::BLEND)
{
mShaderVariant |= LLGLSLShader::GLTFVariant::ALPHA_BLEND;
}
}
createOctree();
return true;
}
void Primitive::upload(LLVertexBuffer* buffer)
{
mVertexBuffer = buffer;
// we store these buffer sizes as S32 elsewhere
llassert(mPositions.size() <= size_t(S32_MAX));
llassert(mIndexArray.size() <= size_t(S32_MAX / 2));
llassert(mVertexBuffer != nullptr);
// assert that buffer can hold this primitive
llassert(mVertexBuffer->getNumVerts() >= mPositions.size() + mVertexOffset);
llassert(mVertexBuffer->getNumIndices() >= mIndexArray.size() + mIndexOffset);
llassert(mVertexBuffer->getTypeMask() == mAttributeMask);
U32 offset = mVertexOffset;
U32 count = getVertexCount();
mVertexBuffer->setPositionData(mPositions.data(), offset, count);
mVertexBuffer->setColorData(mColors.data(), offset, count);
if (!mNormals.empty())
{
mVertexBuffer->setNormalData(mNormals.data(), offset, count);
}
if (!mTangents.empty())
{
mVertexBuffer->setTangentData(mTangents.data(), offset, count);
}
if (!mWeights.empty())
{
mVertexBuffer->setWeight4Data(mWeights.data(), offset, count);
mVertexBuffer->setJointData(mJoints.data(), offset, count);
}
// flip texcoord y, upload, then flip back (keep the off-spec data in vram only)
vertical_flip(mTexCoords0);
mVertexBuffer->setTexCoord0Data(mTexCoords0.data(), offset, count);
vertical_flip(mTexCoords0);
if (!mTexCoords1.empty())
{
vertical_flip(mTexCoords1);
mVertexBuffer->setTexCoord1Data(mTexCoords1.data(), offset, count);
vertical_flip(mTexCoords1);
}
if (!mIndexArray.empty())
{
std::vector<U32> index_array;
index_array.resize(mIndexArray.size());
for (U32 i = 0; i < mIndexArray.size(); ++i)
{
index_array[i] = mIndexArray[i] + mVertexOffset;
}
mVertexBuffer->setIndexData(index_array.data(), mIndexOffset, getIndexCount());
}
}
void initOctreeTriangle(LLVolumeTriangle* tri, F32 scaler, S32 i0, S32 i1, S32 i2, const LLVector4a& v0, const LLVector4a& v1, const LLVector4a& v2)
{
//store pointers to vertex data
tri->mV[0] = &v0;
tri->mV[1] = &v1;
tri->mV[2] = &v2;
//store indices
tri->mIndex[0] = i0;
tri->mIndex[1] = i1;
tri->mIndex[2] = i2;
//get minimum point
LLVector4a min = v0;
min.setMin(min, v1);
min.setMin(min, v2);
//get maximum point
LLVector4a max = v0;
max.setMax(max, v1);
max.setMax(max, v2);
//compute center
LLVector4a center;
center.setAdd(min, max);
center.mul(0.5f);
tri->mPositionGroup = center;
//compute "radius"
LLVector4a size;
size.setSub(max, min);
tri->mRadius = size.getLength3().getF32() * scaler;
}
void Primitive::createOctree()
{
// create octree
mOctree = new LLVolumeOctree();
F32 scaler = 0.25f;
if (mMode == Mode::TRIANGLES)
{
const U32 num_triangles = getIndexCount() / 3;
// Initialize all the triangles we need
mOctreeTriangles.resize(num_triangles);
for (U32 triangle_index = 0; triangle_index < num_triangles; ++triangle_index)
{ //for each triangle
const U32 index = triangle_index * 3;
LLVolumeTriangle* tri = &mOctreeTriangles[triangle_index];
S32 i0 = mIndexArray[index];
S32 i1 = mIndexArray[index + 1];
S32 i2 = mIndexArray[index + 2];
const LLVector4a& v0 = mPositions[i0];
const LLVector4a& v1 = mPositions[i1];
const LLVector4a& v2 = mPositions[i2];
initOctreeTriangle(tri, scaler, i0, i1, i2, v0, v1, v2);
//insert
mOctree->insert(tri);
}
}
else if (mMode == Mode::TRIANGLE_STRIP)
{
const U32 num_triangles = getIndexCount() - 2;
// Initialize all the triangles we need
mOctreeTriangles.resize(num_triangles);
for (U32 triangle_index = 0; triangle_index < num_triangles; ++triangle_index)
{ //for each triangle
const U32 index = triangle_index + 2;
LLVolumeTriangle* tri = &mOctreeTriangles[triangle_index];
S32 i0 = mIndexArray[index];
S32 i1 = mIndexArray[index - 1];
S32 i2 = mIndexArray[index - 2];
const LLVector4a& v0 = mPositions[i0];
const LLVector4a& v1 = mPositions[i1];
const LLVector4a& v2 = mPositions[i2];
initOctreeTriangle(tri, scaler, i0, i1, i2, v0, v1, v2);
//insert
mOctree->insert(tri);
}
}
else if (mMode == Mode::TRIANGLE_FAN)
{
const U32 num_triangles = getIndexCount() - 2;
// Initialize all the triangles we need
mOctreeTriangles.resize(num_triangles);
for (U32 triangle_index = 0; triangle_index < num_triangles; ++triangle_index)
{ //for each triangle
const U32 index = triangle_index + 2;
LLVolumeTriangle* tri = &mOctreeTriangles[triangle_index];
S32 i0 = mIndexArray[0];
S32 i1 = mIndexArray[index - 1];
S32 i2 = mIndexArray[index - 2];
const LLVector4a& v0 = mPositions[i0];
const LLVector4a& v1 = mPositions[i1];
const LLVector4a& v2 = mPositions[i2];
initOctreeTriangle(tri, scaler, i0, i1, i2, v0, v1, v2);
//insert
mOctree->insert(tri);
}
}
else if (mMode == Mode::POINTS ||
mMode == Mode::LINES ||
mMode == Mode::LINE_LOOP ||
mMode == Mode::LINE_STRIP)
{
// nothing to do, no volume... maybe add some collision geometry around these primitive types?
}
else
{
LL_ERRS() << "Unsupported Primitive mode" << LL_ENDL;
}
//remove unneeded octree layers
while (!mOctree->balance()) {}
//calculate AABB for each node
LLVolumeOctreeRebound rebound;
rebound.traverse(mOctree);
}
const LLVolumeTriangle* Primitive::lineSegmentIntersect(const LLVector4a& start, const LLVector4a& end,
LLVector4a* intersection, LLVector2* tex_coord, LLVector4a* normal, LLVector4a* tangent_out)
{
if (mOctree.isNull())
{
return nullptr;
}
LLVector4a dir;
dir.setSub(end, start);
F32 closest_t = 2.f; // must be larger than 1
//create a proxy LLVolumeFace for the raycast
LLVolumeFace face;
face.mPositions = mPositions.data();
face.mTexCoords = mTexCoords0.data();
face.mNormals = mNormals.data();
face.mTangents = mTangents.data();
face.mIndices = nullptr; // unreferenced
face.mNumIndices = S32(mIndexArray.size());
face.mNumVertices = S32(mPositions.size());
LLOctreeTriangleRayIntersect intersect(start, dir, &face, &closest_t, intersection, tex_coord, normal, tangent_out);
intersect.traverse(mOctree);
// null out proxy data so it doesn't get freed
face.mPositions = face.mNormals = face.mTangents = nullptr;
face.mIndices = nullptr;
face.mTexCoords = nullptr;
return intersect.mHitTriangle;
}
Primitive::~Primitive()
{
mOctree = nullptr;
}
LLRender::eGeomModes gltf_mode_to_gl_mode(Primitive::Mode mode)
{
switch (mode)
{
case Primitive::Mode::POINTS:
return LLRender::POINTS;
case Primitive::Mode::LINES:
return LLRender::LINES;
case Primitive::Mode::LINE_LOOP:
return LLRender::LINE_LOOP;
case Primitive::Mode::LINE_STRIP:
return LLRender::LINE_STRIP;
case Primitive::Mode::TRIANGLES:
return LLRender::TRIANGLES;
case Primitive::Mode::TRIANGLE_STRIP:
return LLRender::TRIANGLE_STRIP;
case Primitive::Mode::TRIANGLE_FAN:
return LLRender::TRIANGLE_FAN;
default:
return LLRender::TRIANGLES;
}
}
void Primitive::serialize(boost::json::object& dst) const
{
write(mMaterial, "material", dst, -1);
write(mMode, "mode", dst, Primitive::Mode::TRIANGLES);
write(mIndices, "indices", dst, INVALID_INDEX);
write(mAttributes, "attributes", dst);
}
const Primitive& Primitive::operator=(const Value& src)
{
if (src.is_object())
{
copy(src, "material", mMaterial);
copy(src, "mode", mMode);
copy(src, "indices", mIndices);
copy(src, "attributes", mAttributes);
mGLMode = gltf_mode_to_gl_mode(mMode);
}
return *this;
}
+118
View File
@@ -0,0 +1,118 @@
#pragma once
/**
* @file primitive.h
* @brief LL GLTF Implementation
*
* $LicenseInfo:firstyear=2024&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2024, Linden Research, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
* $/LicenseInfo$
*/
#include "llvertexbuffer.h"
#include "llvolumeoctree.h"
#include "boost/json.hpp"
// LL GLTF Implementation
namespace LL
{
namespace GLTF
{
using Value = boost::json::value;
class Asset;
class Primitive
{
public:
enum class Mode : U8
{
POINTS,
LINES,
LINE_LOOP,
LINE_STRIP,
TRIANGLES,
TRIANGLE_STRIP,
TRIANGLE_FAN
};
~Primitive();
// CPU copy of mesh data
std::vector<LLVector2> mTexCoords0;
std::vector<LLVector2> mTexCoords1;
std::vector<LLVector4a> mNormals;
std::vector<LLVector4a> mTangents;
std::vector<LLVector4a> mPositions;
std::vector<U64> mJoints;
std::vector<LLVector4a> mWeights;
std::vector<LLColor4U> mColors;
std::vector<U32> mIndexArray;
// raycast acceleration structure
LLPointer<LLVolumeOctree> mOctree;
std::vector<LLVolumeTriangle> mOctreeTriangles;
S32 mMaterial = -1;
Mode mMode = Mode::TRIANGLES; // default to triangles
LLRender::eGeomModes mGLMode = LLRender::TRIANGLES; // for use with LLRender
S32 mIndices = -1;
// shader variant according to LLGLSLShader::GLTFVariant flags
U8 mShaderVariant = 0;
// vertex attribute mask
U32 mAttributeMask = 0;
// backpointer to vertex buffer (owned by Asset)
LLPointer<LLVertexBuffer> mVertexBuffer;
U32 mVertexOffset = 0;
U32 mIndexOffset = 0;
U32 getVertexCount() const { return (U32) mPositions.size(); }
U32 getIndexCount() const { return (U32) mIndexArray.size(); }
std::unordered_map<std::string, S32> mAttributes;
// create octree based on vertex buffer
// must be called before buffer is unmapped and after buffer is populated with good data
void createOctree();
//get the LLVolumeTriangle that intersects with the given line segment at the point
//closest to start. Moves end to the point of intersection. Returns nullptr if no intersection.
//Line segment must be in the same coordinate frame as this Primitive
const LLVolumeTriangle* lineSegmentIntersect(const LLVector4a& start, const LLVector4a& end,
LLVector4a* intersection = NULL, // return the intersection point
LLVector2* tex_coord = NULL, // return the texture coordinates of the intersection point
LLVector4a* normal = NULL, // return the surface normal at the intersection point
LLVector4a* tangent = NULL // return the surface tangent at the intersection point
);
void serialize(boost::json::object& obj) const;
const Primitive& operator=(const Value& src);
bool prep(Asset& asset);
// upload geometry to given vertex buffer
// asserts that buffer is bound
// asserts that buffer is valid for this primitive
void upload(LLVertexBuffer* buffer);
};
}
}