Merge pull request #14576 from sabrina-shanman/hfm_prep_graphics

(case 20297) Move graphics preparation from Serializers to runtime preparation step
This commit is contained in:
Adam Smith 2018-12-27 15:46:21 -08:00 committed by GitHub
commit 0d27cb65ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 602 additions and 436 deletions

View file

@ -11,7 +11,7 @@ setup_memory_debugger()
# link in the shared libraries
link_hifi_libraries(
audio avatars octree gpu graphics fbx hfm entities
audio avatars octree gpu graphics shaders fbx hfm entities
networking animation recording shared script-engine embedded-webserver
controllers physics plugins midi image
)

View file

@ -19,20 +19,6 @@
#include <glm/glm.hpp>
#if defined(Q_OS_ANDROID)
#define FBX_PACK_NORMALS 0
#else
#define FBX_PACK_NORMALS 1
#endif
#if FBX_PACK_NORMALS
using NormalType = glm::uint32;
#define FBX_NORMAL_ELEMENT gpu::Element::VEC4F_NORMALIZED_XYZ10W2
#else
using NormalType = glm::vec3;
#define FBX_NORMAL_ELEMENT gpu::Element::VEC3F_XYZ
#endif
// See comment in FBXSerializer::parseFBX().
static const int FBX_HEADER_BYTES_BEFORE_VERSION = 23;
static const QByteArray FBX_BINARY_PROLOG("Kaydara FBX Binary ");

View file

@ -11,27 +11,13 @@
#include "FBXSerializer.h"
#include <iostream>
#include <QBuffer>
#include <QDataStream>
#include <QIODevice>
#include <QStringList>
#include <QTextStream>
#include <QtDebug>
#include <QtEndian>
#include <QFileInfo>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/quaternion.hpp>
#include <glm/gtx/transform.hpp>
#include <FaceshiftConstants.h>
#include <GeometryUtil.h>
#include <GLMHelpers.h>
#include <NumericalConstants.h>
#include <OctalCode.h>
#include <gpu/Format.h>
#include <LogHandler.h>
#include <hfm/ModelFormatLogging.h>
@ -1640,14 +1626,9 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
}
}
}
buildModelMesh(extracted.mesh, url);
hfmModel.meshes.append(extracted.mesh);
int meshIndex = hfmModel.meshes.size() - 1;
if (extracted.mesh._mesh) {
extracted.mesh._mesh->displayName = QString("%1#/mesh/%2").arg(url).arg(meshIndex).toStdString();
extracted.mesh._mesh->modelName = modelIDsToNames.value(modelID).toStdString();
}
meshIDsToMeshIndices.insert(it.key(), meshIndex);
}
@ -1715,22 +1696,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
}
}
}
{
int i = 0;
for (const auto& mesh : hfmModel.meshes) {
auto name = hfmModel.getModelNameOfMesh(i++);
if (!name.isEmpty()) {
if (mesh._mesh) {
mesh._mesh->modelName = name.toStdString();
if (!mesh._mesh->displayName.size()) {
mesh._mesh->displayName = QString("#%1").arg(name).toStdString();
}
} else {
qDebug() << "modelName but no mesh._mesh" << name;
}
}
}
}
auto offsets = getJointRotationOffsets(mapping);
hfmModel.jointRotationOffsets.clear();

View file

@ -111,9 +111,6 @@ public:
static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate = true);
QHash<QString, ExtractedMesh> meshes;
static void buildModelMesh(HFMMesh& extractedMesh, const QString& url);
static glm::vec3 normalizeDirForPacking(const glm::vec3& dir);
HFMTexture getTexture(const QString& textureID);

View file

@ -42,16 +42,6 @@
using vec2h = glm::tvec2<glm::detail::hdata>;
#define HFM_PACK_COLORS 1
#if HFM_PACK_COLORS
using ColorType = glm::uint32;
#define FBX_COLOR_ELEMENT gpu::Element::COLOR_RGBA_32
#else
using ColorType = glm::vec3;
#define FBX_COLOR_ELEMENT gpu::Element::VEC3F_XYZ
#endif
class Vertex {
public:
int originalIndex;
@ -556,364 +546,3 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me
return data.extracted;
}
glm::vec3 FBXSerializer::normalizeDirForPacking(const glm::vec3& dir) {
auto maxCoord = glm::max(fabsf(dir.x), glm::max(fabsf(dir.y), fabsf(dir.z)));
if (maxCoord > 1e-6f) {
return dir / maxCoord;
}
return dir;
}
void FBXSerializer::buildModelMesh(HFMMesh& extractedMesh, const QString& url) {
unsigned int totalSourceIndices = 0;
foreach(const HFMMeshPart& part, extractedMesh.parts) {
totalSourceIndices += (part.quadTrianglesIndices.size() + part.triangleIndices.size());
}
static int repeatMessageID = LogHandler::getInstance().newRepeatedMessageID();
if (!totalSourceIndices) {
HIFI_FCDEBUG_ID(modelformat(), repeatMessageID, "buildModelMesh failed -- no indices, url = " << url);
return;
}
if (extractedMesh.vertices.size() == 0) {
HIFI_FCDEBUG_ID(modelformat(), repeatMessageID, "buildModelMesh failed -- no vertices, url = " << url);
return;
}
HFMMesh& hfmMesh = extractedMesh;
graphics::MeshPointer mesh(new graphics::Mesh());
int numVerts = extractedMesh.vertices.size();
if (!hfmMesh.normals.empty() && hfmMesh.tangents.empty()) {
// Fill with a dummy value to force tangents to be present if there are normals
hfmMesh.tangents.reserve(hfmMesh.normals.size());
std::fill_n(std::back_inserter(hfmMesh.tangents), hfmMesh.normals.size(), Vectors::UNIT_X);
}
// Same thing with blend shapes
for (auto& blendShape : hfmMesh.blendshapes) {
if (!blendShape.normals.empty() && blendShape.tangents.empty()) {
// Fill with a dummy value to force tangents to be present if there are normals
blendShape.tangents.reserve(blendShape.normals.size());
std::fill_n(std::back_inserter(blendShape.tangents), blendShape.normals.size(), Vectors::UNIT_X);
}
}
// evaluate all attribute elements and data sizes
// Position is a vec3
const auto positionElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ);
const int positionsSize = numVerts * positionElement.getSize();
// Normal and tangent are always there together packed in normalized xyz32bits word (times 2)
const auto normalElement = FBX_NORMAL_ELEMENT;
const int normalsSize = hfmMesh.normals.size() * normalElement.getSize();
const int tangentsSize = hfmMesh.tangents.size() * normalElement.getSize();
// If there are normals then there should be tangents
assert(normalsSize <= tangentsSize);
if (tangentsSize > normalsSize) {
qWarning() << "Unexpected tangents in " << url;
}
const auto normalsAndTangentsSize = normalsSize + tangentsSize;
// Color attrib
const auto colorElement = FBX_COLOR_ELEMENT;
const int colorsSize = hfmMesh.colors.size() * colorElement.getSize();
// Texture coordinates are stored in 2 half floats
const auto texCoordsElement = gpu::Element(gpu::VEC2, gpu::HALF, gpu::UV);
const int texCoordsSize = hfmMesh.texCoords.size() * texCoordsElement.getSize();
const int texCoords1Size = hfmMesh.texCoords1.size() * texCoordsElement.getSize();
// Support for 4 skinning clusters:
// 4 Indices are uint8 ideally, uint16 if more than 256.
const auto clusterIndiceElement = (hfmMesh.clusters.size() < UINT8_MAX ? gpu::Element(gpu::VEC4, gpu::UINT8, gpu::XYZW) : gpu::Element(gpu::VEC4, gpu::UINT16, gpu::XYZW));
// 4 Weights are normalized 16bits
const auto clusterWeightElement = gpu::Element(gpu::VEC4, gpu::NUINT16, gpu::XYZW);
// Cluster indices and weights must be the same sizes
const int NUM_CLUSTERS_PER_VERT = 4;
const int numVertClusters = (hfmMesh.clusterIndices.size() == hfmMesh.clusterWeights.size() ? hfmMesh.clusterIndices.size() / NUM_CLUSTERS_PER_VERT : 0);
const int clusterIndicesSize = numVertClusters * clusterIndiceElement.getSize();
const int clusterWeightsSize = numVertClusters * clusterWeightElement.getSize();
// Decide on where to put what seequencially in a big buffer:
const int positionsOffset = 0;
const int normalsAndTangentsOffset = positionsOffset + positionsSize;
const int colorsOffset = normalsAndTangentsOffset + normalsAndTangentsSize;
const int texCoordsOffset = colorsOffset + colorsSize;
const int texCoords1Offset = texCoordsOffset + texCoordsSize;
const int clusterIndicesOffset = texCoords1Offset + texCoords1Size;
const int clusterWeightsOffset = clusterIndicesOffset + clusterIndicesSize;
const int totalVertsSize = clusterWeightsOffset + clusterWeightsSize;
// Copy all vertex data in a single buffer
auto vertBuffer = std::make_shared<gpu::Buffer>();
vertBuffer->resize(totalVertsSize);
// First positions
vertBuffer->setSubData(positionsOffset, positionsSize, (const gpu::Byte*) extractedMesh.vertices.data());
// Interleave normals and tangents
if (normalsSize > 0) {
std::vector<NormalType> normalsAndTangents;
normalsAndTangents.reserve(hfmMesh.normals.size() + hfmMesh.tangents.size());
for (auto normalIt = hfmMesh.normals.constBegin(), tangentIt = hfmMesh.tangents.constBegin();
normalIt != hfmMesh.normals.constEnd();
++normalIt, ++tangentIt) {
#if FBX_PACK_NORMALS
const auto normal = normalizeDirForPacking(*normalIt);
const auto tangent = normalizeDirForPacking(*tangentIt);
const auto packedNormal = glm::packSnorm3x10_1x2(glm::vec4(normal, 0.0f));
const auto packedTangent = glm::packSnorm3x10_1x2(glm::vec4(tangent, 0.0f));
#else
const auto packedNormal = *normalIt;
const auto packedTangent = *tangentIt;
#endif
normalsAndTangents.push_back(packedNormal);
normalsAndTangents.push_back(packedTangent);
}
vertBuffer->setSubData(normalsAndTangentsOffset, normalsAndTangentsSize, (const gpu::Byte*) normalsAndTangents.data());
}
// Pack colors
if (colorsSize > 0) {
#if HFM_PACK_COLORS
std::vector<ColorType> colors;
colors.reserve(hfmMesh.colors.size());
for (const auto& color : hfmMesh.colors) {
colors.push_back(glm::packUnorm4x8(glm::vec4(color, 1.0f)));
}
vertBuffer->setSubData(colorsOffset, colorsSize, (const gpu::Byte*) colors.data());
#else
vertBuffer->setSubData(colorsOffset, colorsSize, (const gpu::Byte*) hfmMesh.colors.constData());
#endif
}
// Pack Texcoords 0 and 1 (if exists)
if (texCoordsSize > 0) {
QVector<vec2h> texCoordData;
texCoordData.reserve(hfmMesh.texCoords.size());
for (auto& texCoordVec2f : hfmMesh.texCoords) {
vec2h texCoordVec2h;
texCoordVec2h.x = glm::detail::toFloat16(texCoordVec2f.x);
texCoordVec2h.y = glm::detail::toFloat16(texCoordVec2f.y);
texCoordData.push_back(texCoordVec2h);
}
vertBuffer->setSubData(texCoordsOffset, texCoordsSize, (const gpu::Byte*) texCoordData.constData());
}
if (texCoords1Size > 0) {
QVector<vec2h> texCoordData;
texCoordData.reserve(hfmMesh.texCoords1.size());
for (auto& texCoordVec2f : hfmMesh.texCoords1) {
vec2h texCoordVec2h;
texCoordVec2h.x = glm::detail::toFloat16(texCoordVec2f.x);
texCoordVec2h.y = glm::detail::toFloat16(texCoordVec2f.y);
texCoordData.push_back(texCoordVec2h);
}
vertBuffer->setSubData(texCoords1Offset, texCoords1Size, (const gpu::Byte*) texCoordData.constData());
}
// Clusters data
if (clusterIndicesSize > 0) {
if (hfmMesh.clusters.size() < UINT8_MAX) {
// yay! we can fit the clusterIndices within 8-bits
int32_t numIndices = hfmMesh.clusterIndices.size();
QVector<uint8_t> clusterIndices;
clusterIndices.resize(numIndices);
for (int32_t i = 0; i < numIndices; ++i) {
assert(hfmMesh.clusterIndices[i] <= UINT8_MAX);
clusterIndices[i] = (uint8_t)(hfmMesh.clusterIndices[i]);
}
vertBuffer->setSubData(clusterIndicesOffset, clusterIndicesSize, (const gpu::Byte*) clusterIndices.constData());
} else {
vertBuffer->setSubData(clusterIndicesOffset, clusterIndicesSize, (const gpu::Byte*) hfmMesh.clusterIndices.constData());
}
}
if (clusterWeightsSize > 0) {
vertBuffer->setSubData(clusterWeightsOffset, clusterWeightsSize, (const gpu::Byte*) hfmMesh.clusterWeights.constData());
}
// Now we decide on how to interleave the attributes and provide the vertices among bufers:
// Aka the Vertex format and the vertexBufferStream
auto vertexFormat = std::make_shared<gpu::Stream::Format>();
auto vertexBufferStream = std::make_shared<gpu::BufferStream>();
// Decision time:
// if blendshapes then keep position and normals/tangents as separated channel buffers from interleaved attributes
// else everything is interleaved in one buffer
// Default case is no blend shapes
gpu::BufferPointer attribBuffer;
int totalAttribBufferSize = totalVertsSize;
gpu::uint8 posChannel = 0;
gpu::uint8 tangentChannel = posChannel;
gpu::uint8 attribChannel = posChannel;
bool interleavePositions = true;
bool interleaveNormalsTangents = true;
// Define the vertex format, compute the offset for each attributes as we append them to the vertex format
gpu::Offset bufOffset = 0;
if (positionsSize) {
vertexFormat->setAttribute(gpu::Stream::POSITION, posChannel, positionElement, bufOffset);
bufOffset += positionElement.getSize();
if (!interleavePositions) {
bufOffset = 0;
}
}
if (normalsSize) {
vertexFormat->setAttribute(gpu::Stream::NORMAL, tangentChannel, normalElement, bufOffset);
bufOffset += normalElement.getSize();
vertexFormat->setAttribute(gpu::Stream::TANGENT, tangentChannel, normalElement, bufOffset);
bufOffset += normalElement.getSize();
if (!interleaveNormalsTangents) {
bufOffset = 0;
}
}
// Pack normal and Tangent with the rest of atributes if no blend shapes
if (colorsSize) {
vertexFormat->setAttribute(gpu::Stream::COLOR, attribChannel, colorElement, bufOffset);
bufOffset += colorElement.getSize();
}
if (texCoordsSize) {
vertexFormat->setAttribute(gpu::Stream::TEXCOORD, attribChannel, texCoordsElement, bufOffset);
bufOffset += texCoordsElement.getSize();
}
if (texCoords1Size) {
vertexFormat->setAttribute(gpu::Stream::TEXCOORD1, attribChannel, texCoordsElement, bufOffset);
bufOffset += texCoordsElement.getSize();
} else if (texCoordsSize) {
vertexFormat->setAttribute(gpu::Stream::TEXCOORD1, attribChannel, texCoordsElement, bufOffset - texCoordsElement.getSize());
}
if (clusterIndicesSize) {
vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, attribChannel, clusterIndiceElement, bufOffset);
bufOffset += clusterIndiceElement.getSize();
}
if (clusterWeightsSize) {
vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, attribChannel, clusterWeightElement, bufOffset);
bufOffset += clusterWeightElement.getSize();
}
// Finally, allocate and fill the attribBuffer interleaving the attributes as needed:
{
auto vPositionOffset = 0;
auto vPositionSize = (interleavePositions ? positionsSize / numVerts : 0);
auto vNormalsAndTangentsOffset = vPositionOffset + vPositionSize;
auto vNormalsAndTangentsSize = (interleaveNormalsTangents ? normalsAndTangentsSize / numVerts : 0);
auto vColorOffset = vNormalsAndTangentsOffset + vNormalsAndTangentsSize;
auto vColorSize = colorsSize / numVerts;
auto vTexcoord0Offset = vColorOffset + vColorSize;
auto vTexcoord0Size = texCoordsSize / numVerts;
auto vTexcoord1Offset = vTexcoord0Offset + vTexcoord0Size;
auto vTexcoord1Size = texCoords1Size / numVerts;
auto vClusterIndiceOffset = vTexcoord1Offset + vTexcoord1Size;
auto vClusterIndiceSize = clusterIndicesSize / numVerts;
auto vClusterWeightOffset = vClusterIndiceOffset + vClusterIndiceSize;
auto vClusterWeightSize = clusterWeightsSize / numVerts;
auto vStride = vClusterWeightOffset + vClusterWeightSize;
std::vector<gpu::Byte> dest;
dest.resize(totalAttribBufferSize);
auto vDest = dest.data();
auto source = vertBuffer->getData();
for (int i = 0; i < numVerts; i++) {
if (vPositionSize) memcpy(vDest + vPositionOffset, source + positionsOffset + i * vPositionSize, vPositionSize);
if (vNormalsAndTangentsSize) memcpy(vDest + vNormalsAndTangentsOffset, source + normalsAndTangentsOffset + i * vNormalsAndTangentsSize, vNormalsAndTangentsSize);
if (vColorSize) memcpy(vDest + vColorOffset, source + colorsOffset + i * vColorSize, vColorSize);
if (vTexcoord0Size) memcpy(vDest + vTexcoord0Offset, source + texCoordsOffset + i * vTexcoord0Size, vTexcoord0Size);
if (vTexcoord1Size) memcpy(vDest + vTexcoord1Offset, source + texCoords1Offset + i * vTexcoord1Size, vTexcoord1Size);
if (vClusterIndiceSize) memcpy(vDest + vClusterIndiceOffset, source + clusterIndicesOffset + i * vClusterIndiceSize, vClusterIndiceSize);
if (vClusterWeightSize) memcpy(vDest + vClusterWeightOffset, source + clusterWeightsOffset + i * vClusterWeightSize, vClusterWeightSize);
vDest += vStride;
}
auto attribBuffer = std::make_shared<gpu::Buffer>();
attribBuffer->setData(totalAttribBufferSize, dest.data());
vertexBufferStream->addBuffer(attribBuffer, 0, vStride);
}
// Mesh vertex format and vertex stream is ready
mesh->setVertexFormatAndStream(vertexFormat, vertexBufferStream);
// Index and Part Buffers
unsigned int totalIndices = 0;
foreach(const HFMMeshPart& part, extractedMesh.parts) {
totalIndices += (part.quadTrianglesIndices.size() + part.triangleIndices.size());
}
if (! totalIndices) {
qCDebug(modelformat) << "buildModelMesh failed -- no indices, url = " << url;
return;
}
auto indexBuffer = std::make_shared<gpu::Buffer>();
indexBuffer->resize(totalIndices * sizeof(int));
int indexNum = 0;
int offset = 0;
std::vector< graphics::Mesh::Part > parts;
if (extractedMesh.parts.size() > 1) {
indexNum = 0;
}
foreach(const HFMMeshPart& part, extractedMesh.parts) {
graphics::Mesh::Part modelPart(indexNum, 0, 0, graphics::Mesh::TRIANGLES);
if (part.quadTrianglesIndices.size()) {
indexBuffer->setSubData(offset,
part.quadTrianglesIndices.size() * sizeof(int),
(gpu::Byte*) part.quadTrianglesIndices.constData());
offset += part.quadTrianglesIndices.size() * sizeof(int);
indexNum += part.quadTrianglesIndices.size();
modelPart._numIndices += part.quadTrianglesIndices.size();
}
if (part.triangleIndices.size()) {
indexBuffer->setSubData(offset,
part.triangleIndices.size() * sizeof(int),
(gpu::Byte*) part.triangleIndices.constData());
offset += part.triangleIndices.size() * sizeof(int);
indexNum += part.triangleIndices.size();
modelPart._numIndices += part.triangleIndices.size();
}
parts.push_back(modelPart);
}
gpu::BufferView indexBufferView(indexBuffer, gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::XYZ));
mesh->setIndexBuffer(indexBufferView);
if (parts.size()) {
auto pb = std::make_shared<gpu::Buffer>();
pb->setData(parts.size() * sizeof(graphics::Mesh::Part), (const gpu::Byte*) parts.data());
gpu::BufferView pbv(pb, gpu::Element(gpu::VEC4, gpu::UINT32, gpu::XYZW));
mesh->setPartBuffer(pbv);
} else {
qCDebug(modelformat) << "buildModelMesh failed -- no parts, url = " << url;
return;
}
// graphics::Box box =
mesh->evalPartBound(0);
extractedMesh._mesh = mesh;
}

View file

@ -890,7 +890,6 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) {
}
mesh.meshIndex = hfmModel.meshes.size();
FBXSerializer::buildModelMesh(mesh, url.toString());
}
}

View file

@ -831,9 +831,6 @@ HFMModel::Pointer OBJSerializer::read(const QByteArray& data, const QVariantHash
hfmModel.meshExtents.addPoint(vertex);
}
// Build the single mesh.
FBXSerializer::buildModelMesh(mesh, _url.toString());
// hfmDebugDump(hfmModel);
} catch(const std::exception& e) {
qCDebug(modelformat) << "OBJSerializer fail: " << e.what();

View file

@ -25,6 +25,30 @@
#include <graphics/Geometry.h>
#include <graphics/Material.h>
#if defined(Q_OS_ANDROID)
#define HFM_PACK_NORMALS 0
#else
#define HFM_PACK_NORMALS 1
#endif
#if HFM_PACK_NORMALS
using NormalType = glm::uint32;
#define HFM_NORMAL_ELEMENT gpu::Element::VEC4F_NORMALIZED_XYZ10W2
#else
using NormalType = glm::vec3;
#define HFM_NORMAL_ELEMENT gpu::Element::VEC3F_XYZ
#endif
#define HFM_PACK_COLORS 1
#if HFM_PACK_COLORS
using ColorType = glm::uint32;
#define HFM_COLOR_ELEMENT gpu::Element::COLOR_RGBA_32
#else
using ColorType = glm::vec3;
#define HFM_COLOR_ELEMENT gpu::Element::VEC3F_XYZ
#endif
const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048;
// High Fidelity Model namespace

View file

@ -1,8 +1,6 @@
set(TARGET_NAME model-baker)
setup_hifi_library()
link_hifi_libraries(shared task)
link_hifi_libraries(shared task gpu graphics)
include_hifi_library_headers(gpu)
include_hifi_library_headers(graphics)
include_hifi_library_headers(hfm)

View file

@ -11,17 +11,89 @@
#include "Baker.h"
#include <shared/HifiTypes.h>
#include "BakerTypes.h"
#include "BuildGraphicsMeshTask.h"
namespace baker {
class GetModelPartsTask {
public:
using Input = hfm::Model::Pointer;
using Output = VaryingSet3<std::vector<hfm::Mesh>, hifi::URL, MeshIndicesToModelNames>;
using JobModel = Job::ModelIO<GetModelPartsTask, Input, Output>;
void run(const BakeContextPointer& context, const Input& input, Output& output) {
auto& hfmModelIn = input;
output.edit0() = hfmModelIn->meshes.toStdVector();
output.edit1() = hfmModelIn->originalURL;
output.edit2() = hfmModelIn->meshIndicesToModelNames;
}
};
class BuildMeshesTask {
public:
using Input = VaryingSet4<std::vector<hfm::Mesh>, std::vector<graphics::MeshPointer>, TangentsPerMesh, BlendshapesPerMesh>;
using Output = std::vector<hfm::Mesh>;
using JobModel = Job::ModelIO<BuildMeshesTask, Input, Output>;
void run(const BakeContextPointer& context, const Input& input, Output& output) {
auto& meshesIn = input.get0();
int numMeshes = (int)meshesIn.size();
auto& graphicsMeshesIn = input.get1();
auto& tangentsPerMeshIn = input.get2();
auto& blendshapesPerMeshIn = input.get3();
auto meshesOut = meshesIn;
for (int i = 0; i < numMeshes; i++) {
auto& meshOut = meshesOut[i];
meshOut._mesh = graphicsMeshesIn[i];
meshOut.tangents = QVector<glm::vec3>::fromStdVector(tangentsPerMeshIn[i]);
meshOut.blendshapes = QVector<hfm::Blendshape>::fromStdVector(blendshapesPerMeshIn[i]);
}
output = meshesOut;
}
};
class BuildModelTask {
public:
using Input = VaryingSet2<hfm::Model::Pointer, std::vector<hfm::Mesh>>;
using Output = hfm::Model::Pointer;
using JobModel = Job::ModelIO<BuildModelTask, Input, Output>;
void run(const BakeContextPointer& context, const Input& input, Output& output) {
auto hfmModelOut = input.get0();
hfmModelOut->meshes = QVector<hfm::Mesh>::fromStdVector(input.get1());
output = hfmModelOut;
}
};
class BakerEngineBuilder {
public:
using Unused = int;
using Input = hfm::Model::Pointer;
using Output = hfm::Model::Pointer;
using JobModel = Task::ModelIO<BakerEngineBuilder, Input, Output>;
void build(JobModel& model, const Varying& in, Varying& out) {
out = in;
void build(JobModel& model, const Varying& hfmModelIn, Varying& hfmModelOut) {
// Split up the inputs from hfm::Model
const auto modelPartsIn = model.addJob<GetModelPartsTask>("GetModelParts", hfmModelIn);
const auto meshesIn = modelPartsIn.getN<GetModelPartsTask::Output>(0);
const auto url = modelPartsIn.getN<GetModelPartsTask::Output>(1);
const auto meshIndicesToModelNames = modelPartsIn.getN<GetModelPartsTask::Output>(2);
// Build the graphics::MeshPointer for each hfm::Mesh
const auto buildGraphicsMeshInputs = BuildGraphicsMeshTask::Input(meshesIn, url, meshIndicesToModelNames).asVarying();
const auto buildGraphicsMeshOutputs = model.addJob<BuildGraphicsMeshTask>("BuildGraphicsMesh", buildGraphicsMeshInputs);
const auto graphicsMeshes = buildGraphicsMeshOutputs.getN<BuildGraphicsMeshTask::Output>(0);
// TODO: Move tangent/blendshape validation/calculation to an earlier step
const auto tangentsPerMesh = buildGraphicsMeshOutputs.getN<BuildGraphicsMeshTask::Output>(1);
const auto blendshapesPerMesh = buildGraphicsMeshOutputs.getN<BuildGraphicsMeshTask::Output>(2);
// Combine the outputs into a new hfm::Model
const auto buildMeshesInputs = BuildMeshesTask::Input(meshesIn, graphicsMeshes, tangentsPerMesh, blendshapesPerMesh).asVarying();
const auto meshesOut = model.addJob<BuildMeshesTask>("BuildMeshes", buildMeshesInputs);
const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut).asVarying();
hfmModelOut = model.addJob<BuildModelTask>("BuildModel", buildModelInputs);
}
};

View file

@ -17,7 +17,6 @@
#include "Engine.h"
namespace baker {
class Baker {
public:
Baker(const hfm::Model::Pointer& hfmModel);

View file

@ -0,0 +1,25 @@
//
// BakerTypes.h
// model-baker/src/model-baker
//
// Created by Sabrina Shanman on 2018/12/10.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_BakerTypes_h
#define hifi_BakerTypes_h
#include <hfm/HFM.h>
namespace baker {
using MeshTangents = std::vector<glm::vec3>;
using TangentsPerMesh = std::vector<std::vector<glm::vec3>>;
using Blendshapes = std::vector<hfm::Blendshape>;
using BlendshapesPerMesh = std::vector<std::vector<hfm::Blendshape>>;
using MeshIndicesToModelNames = QHash<int, QString>;
};
#endif // hifi_BakerTypes_h

View file

@ -0,0 +1,412 @@
//
// BuildGraphicsMeshTask.h
// model-baker/src/model-baker
//
// Created by Sabrina Shanman on 2018/12/06.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "BuildGraphicsMeshTask.h"
#include <glm/gtc/packing.hpp>
#include <LogHandler.h>
#include "ModelBakerLogging.h"
using vec2h = glm::tvec2<glm::detail::hdata>;
glm::vec3 normalizeDirForPacking(const glm::vec3& dir) {
auto maxCoord = glm::max(fabsf(dir.x), glm::max(fabsf(dir.y), fabsf(dir.z)));
if (maxCoord > 1e-6f) {
return dir / maxCoord;
}
return dir;
}
void buildGraphicsMesh(const hfm::Mesh& hfmMesh, graphics::MeshPointer& graphicsMeshPointer, baker::MeshTangents& meshTangents, baker::Blendshapes& blendshapes) {
auto graphicsMesh = std::make_shared<graphics::Mesh>();
unsigned int totalSourceIndices = 0;
foreach(const HFMMeshPart& part, hfmMesh.parts) {
totalSourceIndices += (part.quadTrianglesIndices.size() + part.triangleIndices.size());
}
static int repeatMessageID = LogHandler::getInstance().newRepeatedMessageID();
if (!totalSourceIndices) {
HIFI_FCDEBUG_ID(model_baker(), repeatMessageID, "BuildGraphicsMeshTask failed -- no indices");
return;
}
if (hfmMesh.vertices.size() == 0) {
HIFI_FCDEBUG_ID(model_baker(), repeatMessageID, "BuildGraphicsMeshTask failed -- no vertices");
return;
}
int numVerts = hfmMesh.vertices.size();
if (!hfmMesh.normals.empty() && hfmMesh.tangents.empty()) {
// Fill with a dummy value to force tangents to be present if there are normals
meshTangents.reserve(hfmMesh.normals.size());
std::fill_n(std::back_inserter(meshTangents), hfmMesh.normals.size(), Vectors::UNIT_X);
} else {
meshTangents = hfmMesh.tangents.toStdVector();
}
// Same thing with blend shapes
blendshapes = hfmMesh.blendshapes.toStdVector();
for (auto& blendShape : blendshapes) {
if (!blendShape.normals.empty() && blendShape.tangents.empty()) {
// Fill with a dummy value to force tangents to be present if there are normals
blendShape.tangents.reserve(blendShape.normals.size());
std::fill_n(std::back_inserter(blendShape.tangents), blendShape.normals.size(), Vectors::UNIT_X);
}
}
// evaluate all attribute elements and data sizes
// Position is a vec3
const auto positionElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ);
const int positionsSize = numVerts * positionElement.getSize();
// Normal and tangent are always there together packed in normalized xyz32bits word (times 2)
const auto normalElement = HFM_NORMAL_ELEMENT;
const int normalsSize = hfmMesh.normals.size() * normalElement.getSize();
const int tangentsSize = (int)meshTangents.size() * normalElement.getSize();
// If there are normals then there should be tangents
assert(normalsSize <= tangentsSize);
if (tangentsSize > normalsSize) {
HIFI_FCDEBUG_ID(model_baker(), repeatMessageID, "BuildGraphicsMeshTask -- Unexpected tangents in file");
}
const auto normalsAndTangentsSize = normalsSize + tangentsSize;
// Color attrib
const auto colorElement = HFM_COLOR_ELEMENT;
const int colorsSize = hfmMesh.colors.size() * colorElement.getSize();
// Texture coordinates are stored in 2 half floats
const auto texCoordsElement = gpu::Element(gpu::VEC2, gpu::HALF, gpu::UV);
const int texCoordsSize = hfmMesh.texCoords.size() * texCoordsElement.getSize();
const int texCoords1Size = hfmMesh.texCoords1.size() * texCoordsElement.getSize();
// Support for 4 skinning clusters:
// 4 Indices are uint8 ideally, uint16 if more than 256.
const auto clusterIndiceElement = (hfmMesh.clusters.size() < UINT8_MAX ? gpu::Element(gpu::VEC4, gpu::UINT8, gpu::XYZW) : gpu::Element(gpu::VEC4, gpu::UINT16, gpu::XYZW));
// 4 Weights are normalized 16bits
const auto clusterWeightElement = gpu::Element(gpu::VEC4, gpu::NUINT16, gpu::XYZW);
// Cluster indices and weights must be the same sizes
const int NUM_CLUSTERS_PER_VERT = 4;
const int numVertClusters = (hfmMesh.clusterIndices.size() == hfmMesh.clusterWeights.size() ? hfmMesh.clusterIndices.size() / NUM_CLUSTERS_PER_VERT : 0);
const int clusterIndicesSize = numVertClusters * clusterIndiceElement.getSize();
const int clusterWeightsSize = numVertClusters * clusterWeightElement.getSize();
// Decide on where to put what seequencially in a big buffer:
const int positionsOffset = 0;
const int normalsAndTangentsOffset = positionsOffset + positionsSize;
const int colorsOffset = normalsAndTangentsOffset + normalsAndTangentsSize;
const int texCoordsOffset = colorsOffset + colorsSize;
const int texCoords1Offset = texCoordsOffset + texCoordsSize;
const int clusterIndicesOffset = texCoords1Offset + texCoords1Size;
const int clusterWeightsOffset = clusterIndicesOffset + clusterIndicesSize;
const int totalVertsSize = clusterWeightsOffset + clusterWeightsSize;
// Copy all vertex data in a single buffer
auto vertBuffer = std::make_shared<gpu::Buffer>();
vertBuffer->resize(totalVertsSize);
// First positions
vertBuffer->setSubData(positionsOffset, positionsSize, (const gpu::Byte*) hfmMesh.vertices.data());
// Interleave normals and tangents
if (normalsSize > 0) {
std::vector<NormalType> normalsAndTangents;
normalsAndTangents.reserve(hfmMesh.normals.size() + (int)meshTangents.size());
auto normalIt = hfmMesh.normals.constBegin();
auto tangentIt = meshTangents.cbegin();
for (;
normalIt != hfmMesh.normals.constEnd();
++normalIt, ++tangentIt) {
#if HFM_PACK_NORMALS
const auto normal = normalizeDirForPacking(*normalIt);
const auto tangent = normalizeDirForPacking(*tangentIt);
const auto packedNormal = glm::packSnorm3x10_1x2(glm::vec4(normal, 0.0f));
const auto packedTangent = glm::packSnorm3x10_1x2(glm::vec4(tangent, 0.0f));
#else
const auto packedNormal = *normalIt;
const auto packedTangent = *tangentIt;
#endif
normalsAndTangents.push_back(packedNormal);
normalsAndTangents.push_back(packedTangent);
}
vertBuffer->setSubData(normalsAndTangentsOffset, normalsAndTangentsSize, (const gpu::Byte*) normalsAndTangents.data());
}
// Pack colors
if (colorsSize > 0) {
#if HFM_PACK_COLORS
std::vector<ColorType> colors;
colors.reserve(hfmMesh.colors.size());
for (const auto& color : hfmMesh.colors) {
colors.push_back(glm::packUnorm4x8(glm::vec4(color, 1.0f)));
}
vertBuffer->setSubData(colorsOffset, colorsSize, (const gpu::Byte*) colors.data());
#else
vertBuffer->setSubData(colorsOffset, colorsSize, (const gpu::Byte*) hfmMesh.colors.constData());
#endif
}
// Pack Texcoords 0 and 1 (if exists)
if (texCoordsSize > 0) {
QVector<vec2h> texCoordData;
texCoordData.reserve(hfmMesh.texCoords.size());
for (auto& texCoordVec2f : hfmMesh.texCoords) {
vec2h texCoordVec2h;
texCoordVec2h.x = glm::detail::toFloat16(texCoordVec2f.x);
texCoordVec2h.y = glm::detail::toFloat16(texCoordVec2f.y);
texCoordData.push_back(texCoordVec2h);
}
vertBuffer->setSubData(texCoordsOffset, texCoordsSize, (const gpu::Byte*) texCoordData.constData());
}
if (texCoords1Size > 0) {
QVector<vec2h> texCoordData;
texCoordData.reserve(hfmMesh.texCoords1.size());
for (auto& texCoordVec2f : hfmMesh.texCoords1) {
vec2h texCoordVec2h;
texCoordVec2h.x = glm::detail::toFloat16(texCoordVec2f.x);
texCoordVec2h.y = glm::detail::toFloat16(texCoordVec2f.y);
texCoordData.push_back(texCoordVec2h);
}
vertBuffer->setSubData(texCoords1Offset, texCoords1Size, (const gpu::Byte*) texCoordData.constData());
}
// Clusters data
if (clusterIndicesSize > 0) {
if (hfmMesh.clusters.size() < UINT8_MAX) {
// yay! we can fit the clusterIndices within 8-bits
int32_t numIndices = hfmMesh.clusterIndices.size();
QVector<uint8_t> clusterIndices;
clusterIndices.resize(numIndices);
for (int32_t i = 0; i < numIndices; ++i) {
assert(hfmMesh.clusterIndices[i] <= UINT8_MAX);
clusterIndices[i] = (uint8_t)(hfmMesh.clusterIndices[i]);
}
vertBuffer->setSubData(clusterIndicesOffset, clusterIndicesSize, (const gpu::Byte*) clusterIndices.constData());
} else {
vertBuffer->setSubData(clusterIndicesOffset, clusterIndicesSize, (const gpu::Byte*) hfmMesh.clusterIndices.constData());
}
}
if (clusterWeightsSize > 0) {
vertBuffer->setSubData(clusterWeightsOffset, clusterWeightsSize, (const gpu::Byte*) hfmMesh.clusterWeights.constData());
}
// Now we decide on how to interleave the attributes and provide the vertices among bufers:
// Aka the Vertex format and the vertexBufferStream
auto vertexFormat = std::make_shared<gpu::Stream::Format>();
auto vertexBufferStream = std::make_shared<gpu::BufferStream>();
// Decision time:
// if blendshapes then keep position and normals/tangents as separated channel buffers from interleaved attributes
// else everything is interleaved in one buffer
// Default case is no blend shapes
gpu::BufferPointer attribBuffer;
int totalAttribBufferSize = totalVertsSize;
gpu::uint8 posChannel = 0;
gpu::uint8 tangentChannel = posChannel;
gpu::uint8 attribChannel = posChannel;
bool interleavePositions = true;
bool interleaveNormalsTangents = true;
// Define the vertex format, compute the offset for each attributes as we append them to the vertex format
gpu::Offset bufOffset = 0;
if (positionsSize) {
vertexFormat->setAttribute(gpu::Stream::POSITION, posChannel, positionElement, bufOffset);
bufOffset += positionElement.getSize();
if (!interleavePositions) {
bufOffset = 0;
}
}
if (normalsSize) {
vertexFormat->setAttribute(gpu::Stream::NORMAL, tangentChannel, normalElement, bufOffset);
bufOffset += normalElement.getSize();
vertexFormat->setAttribute(gpu::Stream::TANGENT, tangentChannel, normalElement, bufOffset);
bufOffset += normalElement.getSize();
if (!interleaveNormalsTangents) {
bufOffset = 0;
}
}
// Pack normal and Tangent with the rest of atributes if no blend shapes
if (colorsSize) {
vertexFormat->setAttribute(gpu::Stream::COLOR, attribChannel, colorElement, bufOffset);
bufOffset += colorElement.getSize();
}
if (texCoordsSize) {
vertexFormat->setAttribute(gpu::Stream::TEXCOORD, attribChannel, texCoordsElement, bufOffset);
bufOffset += texCoordsElement.getSize();
}
if (texCoords1Size) {
vertexFormat->setAttribute(gpu::Stream::TEXCOORD1, attribChannel, texCoordsElement, bufOffset);
bufOffset += texCoordsElement.getSize();
} else if (texCoordsSize) {
vertexFormat->setAttribute(gpu::Stream::TEXCOORD1, attribChannel, texCoordsElement, bufOffset - texCoordsElement.getSize());
}
if (clusterIndicesSize) {
vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, attribChannel, clusterIndiceElement, bufOffset);
bufOffset += clusterIndiceElement.getSize();
}
if (clusterWeightsSize) {
vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, attribChannel, clusterWeightElement, bufOffset);
bufOffset += clusterWeightElement.getSize();
}
// Finally, allocate and fill the attribBuffer interleaving the attributes as needed:
{
auto vPositionOffset = 0;
auto vPositionSize = (interleavePositions ? positionsSize / numVerts : 0);
auto vNormalsAndTangentsOffset = vPositionOffset + vPositionSize;
auto vNormalsAndTangentsSize = (interleaveNormalsTangents ? normalsAndTangentsSize / numVerts : 0);
auto vColorOffset = vNormalsAndTangentsOffset + vNormalsAndTangentsSize;
auto vColorSize = colorsSize / numVerts;
auto vTexcoord0Offset = vColorOffset + vColorSize;
auto vTexcoord0Size = texCoordsSize / numVerts;
auto vTexcoord1Offset = vTexcoord0Offset + vTexcoord0Size;
auto vTexcoord1Size = texCoords1Size / numVerts;
auto vClusterIndiceOffset = vTexcoord1Offset + vTexcoord1Size;
auto vClusterIndiceSize = clusterIndicesSize / numVerts;
auto vClusterWeightOffset = vClusterIndiceOffset + vClusterIndiceSize;
auto vClusterWeightSize = clusterWeightsSize / numVerts;
auto vStride = vClusterWeightOffset + vClusterWeightSize;
std::vector<gpu::Byte> dest;
dest.resize(totalAttribBufferSize);
auto vDest = dest.data();
auto source = vertBuffer->getData();
for (int i = 0; i < numVerts; i++) {
if (vPositionSize) memcpy(vDest + vPositionOffset, source + positionsOffset + i * vPositionSize, vPositionSize);
if (vNormalsAndTangentsSize) memcpy(vDest + vNormalsAndTangentsOffset, source + normalsAndTangentsOffset + i * vNormalsAndTangentsSize, vNormalsAndTangentsSize);
if (vColorSize) memcpy(vDest + vColorOffset, source + colorsOffset + i * vColorSize, vColorSize);
if (vTexcoord0Size) memcpy(vDest + vTexcoord0Offset, source + texCoordsOffset + i * vTexcoord0Size, vTexcoord0Size);
if (vTexcoord1Size) memcpy(vDest + vTexcoord1Offset, source + texCoords1Offset + i * vTexcoord1Size, vTexcoord1Size);
if (vClusterIndiceSize) memcpy(vDest + vClusterIndiceOffset, source + clusterIndicesOffset + i * vClusterIndiceSize, vClusterIndiceSize);
if (vClusterWeightSize) memcpy(vDest + vClusterWeightOffset, source + clusterWeightsOffset + i * vClusterWeightSize, vClusterWeightSize);
vDest += vStride;
}
auto attribBuffer = std::make_shared<gpu::Buffer>();
attribBuffer->setData(totalAttribBufferSize, dest.data());
vertexBufferStream->addBuffer(attribBuffer, 0, vStride);
}
// Mesh vertex format and vertex stream is ready
graphicsMesh->setVertexFormatAndStream(vertexFormat, vertexBufferStream);
// Index and Part Buffers
unsigned int totalIndices = 0;
foreach(const HFMMeshPart& part, hfmMesh.parts) {
totalIndices += (part.quadTrianglesIndices.size() + part.triangleIndices.size());
}
if (!totalIndices) {
HIFI_FCDEBUG_ID(model_baker(), repeatMessageID, "BuildGraphicsMeshTask failed -- no indices");
return;
}
auto indexBuffer = std::make_shared<gpu::Buffer>();
indexBuffer->resize(totalIndices * sizeof(int));
int indexNum = 0;
int offset = 0;
std::vector< graphics::Mesh::Part > parts;
if (hfmMesh.parts.size() > 1) {
indexNum = 0;
}
foreach(const HFMMeshPart& part, hfmMesh.parts) {
graphics::Mesh::Part modelPart(indexNum, 0, 0, graphics::Mesh::TRIANGLES);
if (part.quadTrianglesIndices.size()) {
indexBuffer->setSubData(offset,
part.quadTrianglesIndices.size() * sizeof(int),
(gpu::Byte*) part.quadTrianglesIndices.constData());
offset += part.quadTrianglesIndices.size() * sizeof(int);
indexNum += part.quadTrianglesIndices.size();
modelPart._numIndices += part.quadTrianglesIndices.size();
}
if (part.triangleIndices.size()) {
indexBuffer->setSubData(offset,
part.triangleIndices.size() * sizeof(int),
(gpu::Byte*) part.triangleIndices.constData());
offset += part.triangleIndices.size() * sizeof(int);
indexNum += part.triangleIndices.size();
modelPart._numIndices += part.triangleIndices.size();
}
parts.push_back(modelPart);
}
gpu::BufferView indexBufferView(indexBuffer, gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::XYZ));
graphicsMesh->setIndexBuffer(indexBufferView);
if (parts.size()) {
auto pb = std::make_shared<gpu::Buffer>();
pb->setData(parts.size() * sizeof(graphics::Mesh::Part), (const gpu::Byte*) parts.data());
gpu::BufferView pbv(pb, gpu::Element(gpu::VEC4, gpu::UINT32, gpu::XYZW));
graphicsMesh->setPartBuffer(pbv);
} else {
HIFI_FCDEBUG_ID(model_baker(), repeatMessageID, "BuildGraphicsMeshTask failed -- no parts");
return;
}
graphicsMesh->evalPartBound(0);
graphicsMeshPointer = graphicsMesh;
}
void BuildGraphicsMeshTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) {
auto& meshes = input.get0();
auto& url = input.get1();
auto& meshIndicesToModelNames = input.get2();
auto& graphicsMeshes = output.edit0();
auto& tangentsPerMesh = output.edit1();
auto& blendshapesPerMesh = output.edit2();
int n = (int)meshes.size();
for (int i = 0; i < n; i++) {
graphicsMeshes.emplace_back();
auto& graphicsMesh = graphicsMeshes[i];
tangentsPerMesh.emplace_back();
blendshapesPerMesh.emplace_back();
// Try to create the graphics::Mesh
buildGraphicsMesh(meshes[i], graphicsMesh, tangentsPerMesh[i], blendshapesPerMesh[i]);
// Choose a name for the mesh
if (graphicsMesh) {
graphicsMesh->displayName = url.toString().toStdString() + "#/mesh/" + std::to_string(i);
if (meshIndicesToModelNames.find(i) != meshIndicesToModelNames.cend()) {
graphicsMesh->modelName = meshIndicesToModelNames[i].toStdString();
}
}
}
}

View file

@ -0,0 +1,30 @@
//
// BuildGraphicsMeshTask.h
// model-baker/src/model-baker
//
// Created by Sabrina Shanman on 2018/12/06.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_BuildGraphicsMeshTask_h
#define hifi_BuildGraphicsMeshTask_h
#include <hfm/HFM.h>
#include <shared/HifiTypes.h>
#include "Engine.h"
#include "BakerTypes.h"
class BuildGraphicsMeshTask {
public:
using Input = baker::VaryingSet3<std::vector<hfm::Mesh>, hifi::URL, baker::MeshIndicesToModelNames>;
using Output = baker::VaryingSet3<std::vector<graphics::MeshPointer>, std::vector<baker::MeshTangents>, std::vector<baker::Blendshapes>>;
using JobModel = baker::Job::ModelIO<BuildGraphicsMeshTask, Input, Output>;
void run(const baker::BakeContextPointer& context, const Input& input, Output& output);
};
#endif // hifi_BuildGraphicsMeshTask_h

View file

@ -0,0 +1,14 @@
//
// ModelBakerLogging.cpp
// libraries/baker/src/baker
//
// Created by Sabrina Shanman on 2018/12/12.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "ModelBakerLogging.h"
Q_LOGGING_CATEGORY(model_baker, "hifi.model_baker")

View file

@ -0,0 +1,19 @@
//
// ModelBakerLogging.h
// libraries/baker/src/baker
//
// Created by Sabrina Shanman on 2018/12/06.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_ModelBakerLogging_h
#define hifi_ModelBakerLogging_h
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(model_baker)
#endif // hifi_ModelBakerLogging_h