Browse Source

Some work on the GLTF importer

Panagiotis Christopoulos Charitos 6 years ago
parent
commit
035227c7e4

+ 1 - 1
src/anki/core/DeveloperConsole.cpp

@@ -86,7 +86,7 @@ void DeveloperConsole::build(CanvasPtr ctx)
 		ImGui::PopStyleColor();
 		ImGui::PopStyleColor();
 	}
 	}
 
 
-	const U32 timestamp = m_logItemsTimestamp.get();
+	const U32 timestamp = m_logItemsTimestamp.getNonAtomically();
 	const Bool scrollToLast = m_logItemsTimestampConsumed < timestamp;
 	const Bool scrollToLast = m_logItemsTimestampConsumed < timestamp;
 
 
 	if(scrollToLast)
 	if(scrollToLast)

+ 4 - 4
src/anki/gr/common/StackGpuAllocator.cpp

@@ -105,7 +105,7 @@ Error StackGpuAllocator::allocate(PtrSize size, StackGpuAllocatorHandle& handle)
 					alignRoundUp(m_alignment, newChunk->m_size);
 					alignRoundUp(m_alignment, newChunk->m_size);
 
 
 					newChunk->m_next = nullptr;
 					newChunk->m_next = nullptr;
-					newChunk->m_offset.set(0);
+					newChunk->m_offset.setNonAtomically(0);
 					ANKI_CHECK(m_iface->allocate(newChunk->m_size, newChunk->m_mem));
 					ANKI_CHECK(m_iface->allocate(newChunk->m_size, newChunk->m_mem));
 
 
 					m_crntChunk.store(newChunk);
 					m_crntChunk.store(newChunk);
@@ -114,7 +114,7 @@ Error StackGpuAllocator::allocate(PtrSize size, StackGpuAllocatorHandle& handle)
 				{
 				{
 					// Need to recycle one
 					// Need to recycle one
 
 
-					crntChunk->m_next->m_offset.set(0);
+					crntChunk->m_next->m_offset.setNonAtomically(0);
 
 
 					m_crntChunk.store(crntChunk->m_next);
 					m_crntChunk.store(crntChunk->m_next);
 				}
 				}
@@ -127,10 +127,10 @@ Error StackGpuAllocator::allocate(PtrSize size, StackGpuAllocatorHandle& handle)
 
 
 void StackGpuAllocator::reset()
 void StackGpuAllocator::reset()
 {
 {
-	m_crntChunk.set(m_chunkListHead);
+	m_crntChunk.setNonAtomically(m_chunkListHead);
 	if(m_chunkListHead)
 	if(m_chunkListHead)
 	{
 	{
-		m_chunkListHead->m_offset.set(0);
+		m_chunkListHead->m_offset.setNonAtomically(0);
 	}
 	}
 }
 }
 
 

+ 1 - 1
src/anki/gr/vulkan/FenceFactory.cpp

@@ -56,7 +56,7 @@ MicroFence* FenceFactory::newFence()
 		out = m_alloc.newInstance<MicroFence>(this);
 		out = m_alloc.newInstance<MicroFence>(this);
 	}
 	}
 
 
-	ANKI_ASSERT(out->m_refcount.get() == 0);
+	ANKI_ASSERT(out->m_refcount.getNonAtomically() == 0);
 	return out;
 	return out;
 }
 }
 
 

+ 2 - 2
src/anki/gr/vulkan/MicroObjectRecycler.inl.h

@@ -75,7 +75,7 @@ inline T* MicroObjectRecycler<T>::findToReuse()
 			}
 			}
 		}
 		}
 
 
-		ANKI_ASSERT(out->getRefcount().get() == 0);
+		ANKI_ASSERT(out->getRefcount().getNonAtomically() == 0);
 	}
 	}
 
 
 #if ANKI_EXTRA_CHECKS
 #if ANKI_EXTRA_CHECKS
@@ -92,7 +92,7 @@ template<typename T>
 inline void MicroObjectRecycler<T>::recycle(T* s)
 inline void MicroObjectRecycler<T>::recycle(T* s)
 {
 {
 	ANKI_ASSERT(s);
 	ANKI_ASSERT(s);
-	ANKI_ASSERT(s->getRefcount().get() == 0);
+	ANKI_ASSERT(s->getRefcount().getNonAtomically() == 0);
 
 
 	LockGuard<Mutex> lock(m_mtx);
 	LockGuard<Mutex> lock(m_mtx);
 
 

+ 1 - 1
src/anki/gr/vulkan/SemaphoreFactory.inl.h

@@ -57,7 +57,7 @@ inline MicroSemaphorePtr SemaphoreFactory::newInstance(MicroFencePtr fence)
 		out->m_fence = fence;
 		out->m_fence = fence;
 	}
 	}
 
 
-	ANKI_ASSERT(out->m_refcount.get() == 0);
+	ANKI_ASSERT(out->m_refcount.getNonAtomically() == 0);
 	return MicroSemaphorePtr(out);
 	return MicroSemaphorePtr(out);
 }
 }
 
 

+ 36 - 0
src/anki/math/Vec.h

@@ -3089,9 +3089,21 @@ using HVec2 = TVec<F16, 2>;
 /// 32bit signed integer 2D vector
 /// 32bit signed integer 2D vector
 using IVec2 = TVec<I32, 2>;
 using IVec2 = TVec<I32, 2>;
 
 
+/// 16bit signed integer 2D vector
+using I16Vec2 = TVec<I16, 2>;
+
+/// 8bit signed integer 2D vector
+using I8Vec2 = TVec<I8, 2>;
+
 /// 32bit unsigned integer 2D vector
 /// 32bit unsigned integer 2D vector
 using UVec2 = TVec<U32, 2>;
 using UVec2 = TVec<U32, 2>;
 
 
+/// 16bit unsigned integer 2D vector
+using U16Vec2 = TVec<U16, 2>;
+
+/// 8bit unsigned integer 2D vector
+using U8Vec2 = TVec<U8, 2>;
+
 /// F32 3D vector
 /// F32 3D vector
 using Vec3 = TVec<F32, 3>;
 using Vec3 = TVec<F32, 3>;
 static_assert(sizeof(Vec3) == sizeof(F32) * 3, "Incorrect size");
 static_assert(sizeof(Vec3) == sizeof(F32) * 3, "Incorrect size");
@@ -3102,9 +3114,21 @@ using HVec3 = TVec<F16, 3>;
 /// 32bit signed integer 3D vector
 /// 32bit signed integer 3D vector
 using IVec3 = TVec<I32, 3>;
 using IVec3 = TVec<I32, 3>;
 
 
+/// 16bit signed integer 3D vector
+using I16Vec3 = TVec<I16, 3>;
+
+/// 8bit signed integer 3D vector
+using I8Vec3 = TVec<I8, 3>;
+
 /// 32bit unsigned integer 3D vector
 /// 32bit unsigned integer 3D vector
 using UVec3 = TVec<U32, 3>;
 using UVec3 = TVec<U32, 3>;
 
 
+/// 16bit unsigned integer 3D vector
+using U16Vec3 = TVec<U16, 3>;
+
+/// 8bit unsigned integer 3D vector
+using U8Vec3 = TVec<U8, 3>;
+
 /// F32 4D vector
 /// F32 4D vector
 using Vec4 = TVec<F32, 4>;
 using Vec4 = TVec<F32, 4>;
 static_assert(sizeof(Vec4) == sizeof(F32) * 4, "Incorrect size");
 static_assert(sizeof(Vec4) == sizeof(F32) * 4, "Incorrect size");
@@ -3115,8 +3139,20 @@ using HVec4 = TVec<F16, 4>;
 /// 32bit signed integer 4D vector
 /// 32bit signed integer 4D vector
 using IVec4 = TVec<I32, 4>;
 using IVec4 = TVec<I32, 4>;
 
 
+/// 16bit signed integer 4D vector
+using I16Vec4 = TVec<I16, 4>;
+
+/// 8bit signed integer 4D vector
+using I8Vec4 = TVec<I8, 4>;
+
 /// 32bit unsigned integer 4D vector
 /// 32bit unsigned integer 4D vector
 using UVec4 = TVec<U32, 4>;
 using UVec4 = TVec<U32, 4>;
+
+/// 16bit unsigned integer 4D vector
+using U16Vec4 = TVec<U16, 4>;
+
+/// 8bit unsigned integer 4D vector
+using U8Vec4 = TVec<U8, 4>;
 /// @}
 /// @}
 
 
 } // end namespace anki
 } // end namespace anki

+ 1 - 1
src/anki/renderer/MainRenderer.cpp

@@ -95,7 +95,7 @@ Error MainRenderer::render(RenderQueue& rqueue, TexturePtr presentTex)
 	// Run renderer
 	// Run renderer
 	RenderingContext ctx(m_frameAlloc);
 	RenderingContext ctx(m_frameAlloc);
 	m_runCtx.m_ctx = &ctx;
 	m_runCtx.m_ctx = &ctx;
-	m_runCtx.m_secondaryTaskId.set(0);
+	m_runCtx.m_secondaryTaskId.setNonAtomically(0);
 	ctx.m_renderGraphDescr.setStatisticsEnabled(m_statsEnabled);
 	ctx.m_renderGraphDescr.setStatisticsEnabled(m_statsEnabled);
 
 
 	RenderTargetHandle presentRt = ctx.m_renderGraphDescr.importRenderTarget(presentTex, TextureUsageBit::NONE);
 	RenderTargetHandle presentRt = ctx.m_renderGraphDescr.importRenderTarget(presentTex, TextureUsageBit::NONE);

+ 1 - 1
src/anki/scene/Octree.h

@@ -301,7 +301,7 @@ public:
 
 
 	void reset()
 	void reset()
 	{
 	{
-		m_visitedMask.set(0);
+		m_visitedMask.setNonAtomically(0);
 	}
 	}
 
 
 private:
 private:

+ 4 - 4
src/anki/scene/SoftwareRasterizer.cpp

@@ -377,9 +377,9 @@ Bool SoftwareRasterizer::visibilityTestInternal(const Aabb& aabb) const
 	{
 	{
 		for(U x = bboxMin.x(); x < bboxMax.x(); x += 1.0f)
 		for(U x = bboxMin.x(); x < bboxMax.x(); x += 1.0f)
 		{
 		{
-			U idx = U(y) * m_width + U(x);
-			U32 depthi = m_zbuffer[idx].get();
-			F32 depthf = depthi / F32(MAX_U32);
+			const U idx = U(y) * m_width + U(x);
+			const U32 depthi = m_zbuffer[idx].getNonAtomically();
+			const F32 depthf = depthi / F32(MAX_U32);
 			if(minZ < depthf)
 			if(minZ < depthf)
 			{
 			{
 				return true;
 				return true;
@@ -403,7 +403,7 @@ void SoftwareRasterizer::fillDepthBuffer(ConstWeakArray<F32> depthValues)
 		depth = min(depth, 1.0f - EPSILON); // See a few lines above why is that
 		depth = min(depth, 1.0f - EPSILON); // See a few lines above why is that
 
 
 		const U32 depthi = depth * MAX_U32;
 		const U32 depthi = depth * MAX_U32;
-		m_zbuffer[count].set(depthi);
+		m_zbuffer[count].setNonAtomically(depthi);
 	}
 	}
 }
 }
 
 

+ 2 - 2
src/anki/util/Atomic.h

@@ -44,13 +44,13 @@ public:
 	}
 	}
 
 
 	/// Set the value without protection.
 	/// Set the value without protection.
-	void set(const Value& a)
+	void setNonAtomically(const Value& a)
 	{
 	{
 		m_val = a;
 		m_val = a;
 	}
 	}
 
 
 	/// Get the value without protection.
 	/// Get the value without protection.
-	const Value& get() const
+	const Value& getNonAtomically() const
 	{
 	{
 		return m_val;
 		return m_val;
 	}
 	}

+ 23 - 0
src/anki/util/StdTypes.h

@@ -80,6 +80,29 @@ const Second MIN_SECOND = MIN_F64;
 using Timestamp = U64; ///< Timestamp type.
 using Timestamp = U64; ///< Timestamp type.
 const Timestamp MAX_TIMESTAMP = MAX_U64;
 const Timestamp MAX_TIMESTAMP = MAX_U64;
 
 
+/// @name AnKi type literals.
+/// @{
+inline constexpr U8 operator"" _U8(unsigned long long arg) noexcept
+{
+	return static_cast<U8>(arg);
+}
+
+inline constexpr U16 operator"" _U16(unsigned long long arg) noexcept
+{
+	return static_cast<U16>(arg);
+}
+
+inline constexpr U32 operator"" _U32(unsigned long long arg) noexcept
+{
+	return static_cast<U32>(arg);
+}
+
+inline constexpr U64 operator"" _U64(unsigned long long arg) noexcept
+{
+	return static_cast<U64>(arg);
+}
+/// @}
+
 /// Representation of error and a wrapper on top of error codes.
 /// Representation of error and a wrapper on top of error codes.
 class Error
 class Error
 {
 {

+ 1 - 1
src/anki/util/ThreadHive.h

@@ -98,7 +98,7 @@ public:
 		PtrSize alignment = alignof(ThreadHiveSemaphore);
 		PtrSize alignment = alignof(ThreadHiveSemaphore);
 		ThreadHiveSemaphore* sem =
 		ThreadHiveSemaphore* sem =
 			reinterpret_cast<ThreadHiveSemaphore*>(m_alloc.allocate(sizeof(ThreadHiveSemaphore), &alignment));
 			reinterpret_cast<ThreadHiveSemaphore*>(m_alloc.allocate(sizeof(ThreadHiveSemaphore), &alignment));
-		sem->m_atomic.set(initialValue);
+		sem->m_atomic.setNonAtomically(initialValue);
 		return sem;
 		return sem;
 	}
 	}
 
 

+ 1 - 1
tests/resource/AsyncLoader.cpp

@@ -234,7 +234,7 @@ ANKI_TEST(Resource, AsyncLoader)
 		ANKI_TEST_EXPECT_EQ(counter.load(), 4);
 		ANKI_TEST_EXPECT_EQ(counter.load(), 4);
 
 
 		// Check both
 		// Check both
-		counter.set(0);
+		counter.setNonAtomically(0);
 		a.submitNewTask<Task>(0.0, nullptr, &counter, 0, false, false);
 		a.submitNewTask<Task>(0.0, nullptr, &counter, 0, false, false);
 		a.submitNewTask<Task>(0.0, nullptr, &counter, -1, true, true);
 		a.submitNewTask<Task>(0.0, nullptr, &counter, -1, true, true);
 		a.submitNewTask<Task>(0.0, nullptr, &counter, 2, false, false);
 		a.submitNewTask<Task>(0.0, nullptr, &counter, 2, false, false);

+ 5 - 5
tests/util/ThreadHive.cpp

@@ -68,7 +68,7 @@ ANKI_TEST(Util, ThreadHive)
 	if(1)
 	if(1)
 	{
 	{
 		ThreadHiveTestContext ctx;
 		ThreadHiveTestContext ctx;
-		ctx.m_countAtomic.set(0);
+		ctx.m_countAtomic.setNonAtomically(0);
 		const U INITIAL_TASK_COUNT = 100;
 		const U INITIAL_TASK_COUNT = 100;
 
 
 		for(U i = 0; i < INITIAL_TASK_COUNT; ++i)
 		for(U i = 0; i < INITIAL_TASK_COUNT; ++i)
@@ -78,7 +78,7 @@ ANKI_TEST(Util, ThreadHive)
 
 
 		hive.waitAllTasks();
 		hive.waitAllTasks();
 
 
-		ANKI_TEST_EXPECT_EQ(ctx.m_countAtomic.get(), INITIAL_TASK_COUNT * 2);
+		ANKI_TEST_EXPECT_EQ(ctx.m_countAtomic.getNonAtomically(), INITIAL_TASK_COUNT * 2);
 	}
 	}
 
 
 	// Depedency tests
 	// Depedency tests
@@ -121,7 +121,7 @@ ANKI_TEST(Util, ThreadHive)
 
 
 		hive.waitAllTasks();
 		hive.waitAllTasks();
 
 
-		ANKI_TEST_EXPECT_EQ(ctx.m_countAtomic.get(), DEP_TASKS * 2 + 10);
+		ANKI_TEST_EXPECT_EQ(ctx.m_countAtomic.getNonAtomically(), DEP_TASKS * 2 + 10);
 	}
 	}
 
 
 	// Fuzzy test
 	// Fuzzy test
@@ -165,7 +165,7 @@ ANKI_TEST(Util, ThreadHive)
 			hive.waitAllTasks();
 			hive.waitAllTasks();
 		}
 		}
 
 
-		ANKI_TEST_EXPECT_EQ(ctx.m_countAtomic.get(), number);
+		ANKI_TEST_EXPECT_EQ(ctx.m_countAtomic.getNonAtomically(), number);
 	}
 	}
 }
 }
 
 
@@ -242,7 +242,7 @@ ANKI_TEST(Util, ThreadHiveBench)
 	auto timeC = HighRezTimer::getCurrentTime();
 	auto timeC = HighRezTimer::getCurrentTime();
 
 
 	ANKI_TEST_LOGI("Total time %fms. Ground truth %fms", (timeB - timeA) * 1000.0, (timeC - timeB) * 1000.0);
 	ANKI_TEST_LOGI("Total time %fms. Ground truth %fms", (timeB - timeA) * 1000.0, (timeC - timeB) * 1000.0);
-	ANKI_TEST_EXPECT_EQ(sum.get(), serialFib);
+	ANKI_TEST_EXPECT_EQ(sum.getNonAtomically(), serialFib);
 }
 }
 
 
 } // end namespace anki
 } // end namespace anki

+ 157 - 24
tools/gltf_importer/Importer.cpp

@@ -11,6 +11,15 @@
 namespace anki
 namespace anki
 {
 {
 
 
+static F32 computeLightRadius(const Vec3 color)
+{
+	// Based on the attenuation equation: att = 1 - fragLightDist^2 / lightRadius^2
+	const F32 minAtt = 0.01f;
+	const F32 maxIntensity = max(max(color.x(), color.y()), color.z());
+	return sqrt(maxIntensity / minAtt);
+}
+
+#if 0
 static ANKI_USE_RESULT Error getUniformScale(const Mat4& m, F32& out)
 static ANKI_USE_RESULT Error getUniformScale(const Mat4& m, F32& out)
 {
 {
 	const F32 SCALE_THRESHOLD = 0.01f; // 1 cm
 	const F32 SCALE_THRESHOLD = 0.01f; // 1 cm
@@ -29,6 +38,7 @@ static ANKI_USE_RESULT Error getUniformScale(const Mat4& m, F32& out)
 	out = scale;
 	out = scale;
 	return Error::NONE;
 	return Error::NONE;
 }
 }
+#endif
 
 
 static void removeScale(Mat4& m)
 static void removeScale(Mat4& m)
 {
 {
@@ -49,7 +59,17 @@ static void getNodeTransform(const cgltf_node& node, Vec3& tsl, Mat3& rot, Vec3&
 {
 {
 	if(node.has_matrix)
 	if(node.has_matrix)
 	{
 	{
-		ANKI_ASSERT(!"TODO");
+		Mat4 trf = Mat4(node.matrix);
+
+		Vec3 xAxis = trf.getColumn(0).xyz();
+		Vec3 yAxis = trf.getColumn(1).xyz();
+		Vec3 zAxis = trf.getColumn(2).xyz();
+
+		scale = Vec3(xAxis.getLength(), yAxis.getLength(), zAxis.getLength());
+
+		removeScale(trf);
+		rot = trf.getRotationPart();
+		tsl = trf.getTranslationPart().xyz();
 	}
 	}
 	else
 	else
 	{
 	{
@@ -92,7 +112,8 @@ static ANKI_USE_RESULT Error getNodeTransform(const cgltf_node& node, Transform&
 	Vec3 scale;
 	Vec3 scale;
 	getNodeTransform(node, tsl, rot, scale);
 	getNodeTransform(node, tsl, rot, scale);
 
 
-	if(scale[0] != scale[1] || scale[0] != scale[2])
+	const F32 scaleEpsilon = 0.0001f;
+	if(absolute(scale[0] - scale[1]) > scaleEpsilon || absolute(scale[0] - scale[2]) > scaleEpsilon)
 	{
 	{
 		ANKI_GLTF_LOGE("Expecting uniform scale");
 		ANKI_GLTF_LOGE("Expecting uniform scale");
 		return Error::USER_DATA;
 		return Error::USER_DATA;
@@ -148,22 +169,16 @@ Error Importer::load(CString inputFname, CString outDir, CString rpath, CString
 
 
 Error Importer::writeAll()
 Error Importer::writeAll()
 {
 {
-	// Export meshes
-	for(U i = 0; i < m_gltf->meshes_count; ++i)
-	{
-		ANKI_CHECK(writeMesh(m_gltf->meshes[i]));
-	}
+	populateNodePtrToIdx();
 
 
-	// Export materials
-	for(U i = 0; i < m_gltf->materials_count; ++i)
+	for(const cgltf_animation* anim = m_gltf->animations; anim < m_gltf->animations + m_gltf->animations_count; ++anim)
 	{
 	{
-		ANKI_CHECK(writeMaterial(m_gltf->materials[i]));
+		ANKI_CHECK(writeAnimation(*anim));
 	}
 	}
 
 
 	StringAuto sceneFname(m_alloc);
 	StringAuto sceneFname(m_alloc);
 	sceneFname.sprintf("%sscene.lua", m_outDir.cstr());
 	sceneFname.sprintf("%sscene.lua", m_outDir.cstr());
 	ANKI_CHECK(m_sceneFile.open(sceneFname.toCString(), FileOpenFlag::WRITE));
 	ANKI_CHECK(m_sceneFile.open(sceneFname.toCString(), FileOpenFlag::WRITE));
-
 	ANKI_CHECK(m_sceneFile.writeText("local scene = getSceneGraph()\nlocal events = getEventManager()\n"));
 	ANKI_CHECK(m_sceneFile.writeText("local scene = getSceneGraph()\nlocal events = getEventManager()\n"));
 
 
 	// Nodes
 	// Nodes
@@ -175,6 +190,16 @@ Error Importer::writeAll()
 		}
 		}
 	}
 	}
 
 
+	m_hive->waitAllTasks();
+
+	// Check error
+	const Error threadErr = m_errorInThread.load();
+	if(threadErr)
+	{
+		ANKI_GLTF_LOGE("Error happened in a thread");
+		return threadErr;
+	}
+
 	return Error::NONE;
 	return Error::NONE;
 }
 }
 
 
@@ -249,6 +274,47 @@ Error Importer::getExtras(const cgltf_extras& extras, HashMapAuto<CString, Strin
 	return Error::NONE;
 	return Error::NONE;
 }
 }
 
 
+void Importer::populateNodePtrToIdx(const cgltf_node& node, U& idx)
+{
+	m_nodePtrToIdx.emplace(&node, idx++);
+
+	for(cgltf_node* const* c = node.children; c < node.children + node.children_count; ++c)
+	{
+		populateNodePtrToIdx(**c, idx);
+	}
+}
+
+void Importer::populateNodePtrToIdx()
+{
+	U idx = 0;
+
+	for(const cgltf_scene* scene = m_gltf->scenes; scene < m_gltf->scenes + m_gltf->scenes_count; ++scene)
+	{
+		for(cgltf_node* const* node = scene->nodes; node < scene->nodes + scene->nodes_count; ++node)
+		{
+			populateNodePtrToIdx(**node, idx);
+		}
+	}
+}
+
+StringAuto Importer::getNodeName(const cgltf_node& node)
+{
+	StringAuto out{m_alloc};
+
+	if(node.name)
+	{
+		out.create(node.name);
+	}
+	else
+	{
+		auto it = m_nodePtrToIdx.find(&node);
+		ANKI_ASSERT(it != m_nodePtrToIdx.getEnd());
+		out.sprintf("unnamed_node_%u", *it);
+	}
+
+	return out;
+}
+
 Error Importer::parseArrayOfNumbers(CString str, DynamicArrayAuto<F64>& out, const U* expectedArraySize)
 Error Importer::parseArrayOfNumbers(CString str, DynamicArrayAuto<F64>& out, const U* expectedArraySize)
 {
 {
 	StringListAuto list(m_alloc);
 	StringListAuto list(m_alloc);
@@ -283,6 +349,14 @@ Error Importer::parseArrayOfNumbers(CString str, DynamicArrayAuto<F64>& out, con
 Error Importer::visitNode(
 Error Importer::visitNode(
 	const cgltf_node& node, const Transform& parentTrf, const HashMapAuto<CString, StringAuto>& parentExtras)
 	const cgltf_node& node, const Transform& parentTrf, const HashMapAuto<CString, StringAuto>& parentExtras)
 {
 {
+	// Check error from a thread
+	const Error threadErr = m_errorInThread.load();
+	if(threadErr)
+	{
+		ANKI_GLTF_LOGE("Error happened in a thread");
+		return threadErr;
+	}
+
 	Transform localTrf;
 	Transform localTrf;
 	ANKI_CHECK(getNodeTransform(node, localTrf));
 	ANKI_CHECK(getNodeTransform(node, localTrf));
 
 
@@ -315,7 +389,7 @@ Error Importer::visitNode(
 
 
 			ANKI_CHECK(m_sceneFile.writeText("\nnode = scene:new%sModelNode(\"%s\", \"%s%s\")\n",
 			ANKI_CHECK(m_sceneFile.writeText("\nnode = scene:new%sModelNode(\"%s\", \"%s%s\")\n",
 				(gpuParticles) ? "Gpu" : "",
 				(gpuParticles) ? "Gpu" : "",
-				node.name,
+				getNodeName(node).cstr(),
 				m_rpath.cstr(),
 				m_rpath.cstr(),
 				fname.cstr()));
 				fname.cstr()));
 		}
 		}
@@ -335,7 +409,7 @@ Error Importer::visitNode(
 			}
 			}
 
 
 			ANKI_CHECK(m_sceneFile.writeText("\nnode = scene:newStaticCollisionNode(\"%s\", \"%s%s.ankicl\")\n",
 			ANKI_CHECK(m_sceneFile.writeText("\nnode = scene:newStaticCollisionNode(\"%s\", \"%s%s.ankicl\")\n",
-				node.name,
+				getNodeName(node).cstr(),
 				m_rpath.cstr(),
 				m_rpath.cstr(),
 				node.mesh->name));
 				node.mesh->name));
 		}
 		}
@@ -354,7 +428,7 @@ Error Importer::visitNode(
 
 
 			ANKI_CHECK(m_sceneFile.writeText(
 			ANKI_CHECK(m_sceneFile.writeText(
 				"\nnode = scene:newReflectionProbeNode(\"%s\", Vec4.new(%f, %f, %f, 0), Vec4.new(%f, %f, %f, 0))\n",
 				"\nnode = scene:newReflectionProbeNode(\"%s\", Vec4.new(%f, %f, %f, 0), Vec4.new(%f, %f, %f, 0))\n",
-				node.name,
+				getNodeName(node).cstr(),
 				aabbMin.x(),
 				aabbMin.x(),
 				aabbMin.y(),
 				aabbMin.y(),
 				aabbMin.z(),
 				aabbMin.z(),
@@ -387,7 +461,8 @@ Error Importer::visitNode(
 				ANKI_CHECK(it->toNumber(cellSize));
 				ANKI_CHECK(it->toNumber(cellSize));
 			}
 			}
 
 
-			ANKI_CHECK(m_sceneFile.writeText("\nnode = scene:newGlobalIlluminationProbeNode(\"%s\")\n", node.name));
+			ANKI_CHECK(m_sceneFile.writeText(
+				"\nnode = scene:newGlobalIlluminationProbeNode(\"%s\")\n", getNodeName(node).cstr()));
 			ANKI_CHECK(m_sceneFile.writeText("comp = node:getSceneNodeBase():getGlobalIlluminationProbeComponent()\n"));
 			ANKI_CHECK(m_sceneFile.writeText("comp = node:getSceneNodeBase():getGlobalIlluminationProbeComponent()\n"));
 
 
 			ANKI_CHECK(m_sceneFile.writeText("comp:setBoundingBox(Vec4.new(%f, %f, %f, 0), Vec4.new(%f, %f, %f, 0))\n",
 			ANKI_CHECK(m_sceneFile.writeText("comp:setBoundingBox(Vec4.new(%f, %f, %f, 0), Vec4.new(%f, %f, %f, 0))\n",
@@ -415,6 +490,8 @@ Error Importer::visitNode(
 			Vec3 scale;
 			Vec3 scale;
 			getNodeTransform(node, tsl, rot, scale);
 			getNodeTransform(node, tsl, rot, scale);
 
 
+			localTrf = Transform(tsl.xyz0(), Mat3x4(rot), 1.0f);
+
 			StringAuto diffuseAtlas(m_alloc);
 			StringAuto diffuseAtlas(m_alloc);
 			if((it = extras.find("decal_diffuse_atlas")) != extras.getEnd())
 			if((it = extras.find("decal_diffuse_atlas")) != extras.getEnd())
 			{
 			{
@@ -451,7 +528,7 @@ Error Importer::visitNode(
 				ANKI_CHECK(it->toNumber(specularRougnessMetallicFactor));
 				ANKI_CHECK(it->toNumber(specularRougnessMetallicFactor));
 			}
 			}
 
 
-			ANKI_CHECK(m_sceneFile.writeText("\nnode = scene:newDecalNode(\"%s\")\n", node.name));
+			ANKI_CHECK(m_sceneFile.writeText("\nnode = scene:newDecalNode(\"%s\")\n", getNodeName(node).cstr()));
 			ANKI_CHECK(m_sceneFile.writeText("comp = node:getSceneNodeBase():getDecalComponent()\n"));
 			ANKI_CHECK(m_sceneFile.writeText("comp = node:getSceneNodeBase():getDecalComponent()\n"));
 			if(diffuseAtlas)
 			if(diffuseAtlas)
 			{
 			{
@@ -471,13 +548,39 @@ Error Importer::visitNode(
 		}
 		}
 		else
 		else
 		{
 		{
+			// Async because it's slow
+			struct Ctx
+			{
+				Importer* m_importer;
+				cgltf_mesh* m_mesh;
+			};
+			Ctx* ctx = m_alloc.newInstance<Ctx>();
+			ctx->m_importer = this;
+			ctx->m_mesh = node.mesh;
+
+			m_hive->submitTask(
+				[](void* userData, U32 threadId, ThreadHive& hive, ThreadHiveSemaphore* signalSemaphore) {
+					Ctx& self = *static_cast<Ctx*>(userData);
+
+					const Error err = self.m_importer->writeMesh(*self.m_mesh);
+					if(err)
+					{
+						self.m_importer->m_errorInThread.store(err._getCode());
+					}
+
+					self.m_importer->m_alloc.deleteInstance(&self);
+				},
+				ctx);
+
+			ANKI_CHECK(writeMaterial(*node.mesh->primitives[0].material));
 			ANKI_CHECK(writeModel(*node.mesh));
 			ANKI_CHECK(writeModel(*node.mesh));
+
 			ANKI_CHECK(writeModelNode(node, parentExtras));
 			ANKI_CHECK(writeModelNode(node, parentExtras));
 		}
 		}
 	}
 	}
 	else
 	else
 	{
 	{
-		ANKI_GLTF_LOGW("Ignoring node %s. Assuming transform node", node.name);
+		ANKI_GLTF_LOGW("Ignoring node %s. Assuming transform node", getNodeName(node).cstr());
 		ANKI_CHECK(getExtras(node.extras, outExtras));
 		ANKI_CHECK(getExtras(node.extras, outExtras));
 		dummyNode = true;
 		dummyNode = true;
 	}
 	}
@@ -523,7 +626,7 @@ Error Importer::writeModel(const cgltf_mesh& mesh)
 {
 {
 	StringAuto modelFname(m_alloc);
 	StringAuto modelFname(m_alloc);
 	modelFname.sprintf("%s%s_%s.ankimdl", m_outDir.cstr(), mesh.name, mesh.primitives[0].material->name);
 	modelFname.sprintf("%s%s_%s.ankimdl", m_outDir.cstr(), mesh.name, mesh.primitives[0].material->name);
-	ANKI_GLTF_LOGI("Exporting model %s", modelFname.cstr());
+	ANKI_GLTF_LOGI("Importing model %s", modelFname.cstr());
 
 
 	if(mesh.primitives_count != 1)
 	if(mesh.primitives_count != 1)
 	{
 	{
@@ -571,10 +674,39 @@ Error Importer::writeModel(const cgltf_mesh& mesh)
 	return Error::NONE;
 	return Error::NONE;
 }
 }
 
 
+Error Importer::writeAnimation(const cgltf_animation& anim)
+{
+	StringAuto fname(m_alloc);
+	fname.sprintf("%s%s.ankianim", m_outDir.cstr(), anim.name);
+	ANKI_GLTF_LOGI("Importing animation %s", fname.cstr());
+
+	File file;
+	ANKI_CHECK(file.open(fname.toCString(), FileOpenFlag::WRITE));
+
+	ANKI_CHECK(file.writeText("<animation>\n"));
+	ANKI_CHECK(file.writeText("\t<channels>\n"));
+
+	for(U i = 0; i < anim.channels_count; ++i)
+	{
+		const cgltf_animation_channel& channel = anim.channels[i];
+
+		ANKI_CHECK(file.writeText("\t\t<channel>\n"));
+
+		ANKI_CHECK(file.writeText("\t\t\t<name>%s</name>\n", getNodeName(*channel.target_node).cstr()));
+
+		ANKI_CHECK(file.writeText("\t\t<channel>\n"));
+	}
+
+	ANKI_CHECK(file.writeText("\t</channels>\n"));
+	ANKI_CHECK(file.writeText("</animation>\n"));
+
+	return Error::NONE;
+}
+
 Error Importer::writeLight(const cgltf_node& node, const HashMapAuto<CString, StringAuto>& parentExtras)
 Error Importer::writeLight(const cgltf_node& node, const HashMapAuto<CString, StringAuto>& parentExtras)
 {
 {
 	const cgltf_light& light = *node.light;
 	const cgltf_light& light = *node.light;
-	ANKI_GLTF_LOGI("Exporting light %s", light.name);
+	ANKI_GLTF_LOGI("Importing light %s", light.name);
 
 
 	HashMapAuto<CString, StringAuto> extras(parentExtras);
 	HashMapAuto<CString, StringAuto> extras(parentExtras);
 	ANKI_CHECK(getExtras(node.extras, extras));
 	ANKI_CHECK(getExtras(node.extras, extras));
@@ -698,14 +830,14 @@ Error Importer::writeCamera(const cgltf_node& node, const HashMapAuto<CString, S
 {
 {
 	if(node.camera->type != cgltf_camera_type_perspective)
 	if(node.camera->type != cgltf_camera_type_perspective)
 	{
 	{
-		ANKI_GLTF_LOGW("Unsupported camera type: %s", node.name);
+		ANKI_GLTF_LOGW("Unsupported camera type: %s", getNodeName(node).cstr());
 		return Error::NONE;
 		return Error::NONE;
 	}
 	}
 
 
 	const cgltf_camera_perspective& cam = node.camera->perspective;
 	const cgltf_camera_perspective& cam = node.camera->perspective;
-	ANKI_GLTF_LOGI("Exporting camera %s", node.name);
+	ANKI_GLTF_LOGI("Importing camera %s", getNodeName(node).cstr());
 
 
-	ANKI_CHECK(m_sceneFile.writeText("\nnode = scene:newPerspectiveCameraNode(\"%s\")\n", node.name));
+	ANKI_CHECK(m_sceneFile.writeText("\nnode = scene:newPerspectiveCameraNode(\"%s\")\n", getNodeName(node).cstr()));
 	ANKI_CHECK(m_sceneFile.writeText("scene:setActiveCameraNode(node:getSceneNodeBase())\n"));
 	ANKI_CHECK(m_sceneFile.writeText("scene:setActiveCameraNode(node:getSceneNodeBase())\n"));
 	ANKI_CHECK(m_sceneFile.writeText("frustumc = node:getSceneNodeBase():getFrustumComponent()\n"));
 	ANKI_CHECK(m_sceneFile.writeText("frustumc = node:getSceneNodeBase():getFrustumComponent()\n"));
 
 
@@ -720,7 +852,7 @@ Error Importer::writeCamera(const cgltf_node& node, const HashMapAuto<CString, S
 
 
 Error Importer::writeModelNode(const cgltf_node& node, const HashMapAuto<CString, StringAuto>& parentExtras)
 Error Importer::writeModelNode(const cgltf_node& node, const HashMapAuto<CString, StringAuto>& parentExtras)
 {
 {
-	ANKI_GLTF_LOGI("Exporting model node %s", node.name);
+	ANKI_GLTF_LOGI("Importing model node %s", getNodeName(node).cstr());
 
 
 	HashMapAuto<CString, StringAuto> extras(parentExtras);
 	HashMapAuto<CString, StringAuto> extras(parentExtras);
 	ANKI_CHECK(getExtras(node.extras, extras));
 	ANKI_CHECK(getExtras(node.extras, extras));
@@ -728,7 +860,8 @@ Error Importer::writeModelNode(const cgltf_node& node, const HashMapAuto<CString
 	StringAuto modelFname(m_alloc);
 	StringAuto modelFname(m_alloc);
 	modelFname.sprintf("%s%s_%s.ankimdl", m_rpath.cstr(), node.mesh->name, node.mesh->primitives[0].material->name);
 	modelFname.sprintf("%s%s_%s.ankimdl", m_rpath.cstr(), node.mesh->name, node.mesh->primitives[0].material->name);
 
 
-	ANKI_CHECK(m_sceneFile.writeText("\nnode = scene:newModelNode(\"%s\", \"%s\")\n", node.name, modelFname.cstr()));
+	ANKI_CHECK(m_sceneFile.writeText(
+		"\nnode = scene:newModelNode(\"%s\", \"%s\")\n", getNodeName(node).cstr(), modelFname.cstr()));
 
 
 	// TODO: collision mesh
 	// TODO: collision mesh
 
 

+ 17 - 8
tools/gltf_importer/Importer.h

@@ -39,21 +39,30 @@ private:
 
 
 	File m_sceneFile;
 	File m_sceneFile;
 
 
+	Atomic<I32> m_errorInThread{0};
+
+	class PtrHasher
+	{
+	public:
+		U64 operator()(const void* ptr)
+		{
+			return computeHash(&ptr, sizeof(ptr));
+		}
+	};
+
+	HashMapAuto<const void*, U32, PtrHasher> m_nodePtrToIdx{m_alloc}; ///< Need an index for the unnamed nodes.
+
 	ANKI_USE_RESULT Error getExtras(const cgltf_extras& extras, HashMapAuto<CString, StringAuto>& out);
 	ANKI_USE_RESULT Error getExtras(const cgltf_extras& extras, HashMapAuto<CString, StringAuto>& out);
 	ANKI_USE_RESULT Error parseArrayOfNumbers(
 	ANKI_USE_RESULT Error parseArrayOfNumbers(
 		CString str, DynamicArrayAuto<F64>& out, const U* expectedArraySize = nullptr);
 		CString str, DynamicArrayAuto<F64>& out, const U* expectedArraySize = nullptr);
-
-	static F32 computeLightRadius(const Vec3 color)
-	{
-		// Based on the attenuation equation: att = 1 - fragLightDist^2 / lightRadius^2
-		const F32 minAtt = 0.01f;
-		const F32 maxIntensity = max(max(color.x(), color.y()), color.z());
-		return sqrt(maxIntensity / minAtt);
-	}
+	void populateNodePtrToIdx();
+	void populateNodePtrToIdx(const cgltf_node& node, U& idx);
+	StringAuto getNodeName(const cgltf_node& node);
 
 
 	ANKI_USE_RESULT Error writeMesh(const cgltf_mesh& mesh);
 	ANKI_USE_RESULT Error writeMesh(const cgltf_mesh& mesh);
 	ANKI_USE_RESULT Error writeMaterial(const cgltf_material& mtl);
 	ANKI_USE_RESULT Error writeMaterial(const cgltf_material& mtl);
 	ANKI_USE_RESULT Error writeModel(const cgltf_mesh& mesh);
 	ANKI_USE_RESULT Error writeModel(const cgltf_mesh& mesh);
+	ANKI_USE_RESULT Error writeAnimation(const cgltf_animation& anim);
 
 
 	// Scene
 	// Scene
 	ANKI_USE_RESULT Error writeTransform(const Transform& trf);
 	ANKI_USE_RESULT Error writeTransform(const Transform& trf);

+ 1 - 1
tools/gltf_importer/ImporterMaterial.cpp

@@ -70,7 +70,7 @@ Error Importer::writeMaterial(const cgltf_material& mtl)
 {
 {
 	StringAuto fname(m_alloc);
 	StringAuto fname(m_alloc);
 	fname.sprintf("%s%s.ankimtl", m_outDir.cstr(), mtl.name);
 	fname.sprintf("%s%s.ankimtl", m_outDir.cstr(), mtl.name);
-	ANKI_GLTF_LOGI("Exporting %s", fname.cstr());
+	ANKI_GLTF_LOGI("Importing material %s", fname.cstr());
 
 
 	if(!mtl.has_pbr_metallic_roughness)
 	if(!mtl.has_pbr_metallic_roughness)
 	{
 	{

+ 95 - 5
tools/gltf_importer/ImporterMesh.cpp

@@ -41,6 +41,9 @@ static U cgltfComponentSize(cgltf_component_type type)
 	case cgltf_component_type_r_32f:
 	case cgltf_component_type_r_32f:
 		out = sizeof(F32);
 		out = sizeof(F32);
 		break;
 		break;
+	case cgltf_component_type_r_16u:
+		out = sizeof(U16);
+		break;
 	default:
 	default:
 		ANKI_ASSERT(!"TODO");
 		ANKI_ASSERT(!"TODO");
 		out = 0;
 		out = 0;
@@ -63,6 +66,12 @@ static Error appendAttribute(const cgltf_attribute& attrib, DynamicArrayAuto<T>&
 		return Error::USER_DATA;
 		return Error::USER_DATA;
 	}
 	}
 
 
+	if(cgltfComponentSize(attrib.data->component_type) != sizeof(typename T::Scalar))
+	{
+		ANKI_GLTF_LOGE("Incompatible type: %s", attrib.name);
+		return Error::USER_DATA;
+	}
+
 	ANKI_ASSERT(attrib.data);
 	ANKI_ASSERT(attrib.data);
 
 
 	const U8* base = static_cast<const U8*>(attrib.data->buffer_view->buffer->data) + attrib.data->offset
 	const U8* base = static_cast<const U8*>(attrib.data->buffer_view->buffer->data) + attrib.data->offset
@@ -97,6 +106,8 @@ public:
 	DynamicArrayAuto<Vec3> m_normals;
 	DynamicArrayAuto<Vec3> m_normals;
 	DynamicArrayAuto<Vec4> m_tangents;
 	DynamicArrayAuto<Vec4> m_tangents;
 	DynamicArrayAuto<Vec2> m_uvs;
 	DynamicArrayAuto<Vec2> m_uvs;
+	DynamicArrayAuto<U16Vec4> m_boneIds;
+	DynamicArrayAuto<Vec4> m_boneWeights;
 	DynamicArrayAuto<U16> m_indices;
 	DynamicArrayAuto<U16> m_indices;
 
 
 	Vec3 m_aabbMin{MAX_F32};
 	Vec3 m_aabbMin{MAX_F32};
@@ -110,16 +121,24 @@ public:
 		, m_normals(alloc)
 		, m_normals(alloc)
 		, m_tangents(alloc)
 		, m_tangents(alloc)
 		, m_uvs(alloc)
 		, m_uvs(alloc)
+		, m_boneIds(alloc)
+		, m_boneWeights(alloc)
 		, m_indices(alloc)
 		, m_indices(alloc)
 	{
 	{
 	}
 	}
 };
 };
 
 
+struct WeightVertex
+{
+	U16Vec4 m_boneIndices{MAX_U16};
+	U8Vec4 m_weights{0_U8};
+};
+
 Error Importer::writeMesh(const cgltf_mesh& mesh)
 Error Importer::writeMesh(const cgltf_mesh& mesh)
 {
 {
 	StringAuto fname(m_alloc);
 	StringAuto fname(m_alloc);
 	fname.sprintf("%s%s.ankimesh", m_outDir.cstr(), mesh.name);
 	fname.sprintf("%s%s.ankimesh", m_outDir.cstr(), mesh.name);
-	ANKI_GLTF_LOGI("Exporting %s", fname.cstr());
+	ANKI_GLTF_LOGI("Importing mesh %s", fname.cstr());
 
 
 	DynamicArrayAuto<SubMesh> submeshes(m_alloc);
 	DynamicArrayAuto<SubMesh> submeshes(m_alloc);
 	U totalVertCount = 0;
 	U totalVertCount = 0;
@@ -128,6 +147,7 @@ Error Importer::writeMesh(const cgltf_mesh& mesh)
 	Vec3 aabbMax(MIN_F32);
 	Vec3 aabbMax(MIN_F32);
 	F32 maxUvDistance = MIN_F32;
 	F32 maxUvDistance = MIN_F32;
 	F32 minUvDistance = MAX_F32;
 	F32 minUvDistance = MAX_F32;
+	Bool hasBoneWeights = false;
 
 
 	// Iterate primitives. Every primitive is a submesh
 	// Iterate primitives. Every primitive is a submesh
 	for(const cgltf_primitive* primitive = mesh.primitives; primitive < mesh.primitives + mesh.primitives_count;
 	for(const cgltf_primitive* primitive = mesh.primitives; primitive < mesh.primitives + mesh.primitives_count;
@@ -166,6 +186,15 @@ Error Importer::writeMesh(const cgltf_mesh& mesh)
 					minUvDistance = min(minUvDistance, min(uv.x(), uv.y()));
 					minUvDistance = min(minUvDistance, min(uv.x(), uv.y()));
 				});
 				});
 			}
 			}
+			else if(attrib->type == cgltf_attribute_type_joints)
+			{
+				appendAttribute(*attrib, submesh.m_boneIds, [](const U16Vec4&) {});
+				hasBoneWeights = true;
+			}
+			else if(attrib->type == cgltf_attribute_type_weights)
+			{
+				appendAttribute(*attrib, submesh.m_boneWeights, [](const Vec4&) {});
+			}
 			else
 			else
 			{
 			{
 				ANKI_GLTF_LOGW("Ignoring attribute: %s", attrib->name);
 				ANKI_GLTF_LOGW("Ignoring attribute: %s", attrib->name);
@@ -194,8 +223,21 @@ Error Importer::writeMesh(const cgltf_mesh& mesh)
 			return Error::USER_DATA;
 			return Error::USER_DATA;
 		}
 		}
 
 
+		if(submesh.m_boneIds.getSize() != 0 && submesh.m_boneIds.getSize() != vertCount)
+		{
+			ANKI_GLTF_LOGE("Bone IDs count is incorrect. Is %u, should be %u", submesh.m_boneIds.getSize(), vertCount);
+			return Error::USER_DATA;
+		}
+
+		if(submesh.m_boneWeights.getSize() != 0 && submesh.m_boneWeights.getSize() != vertCount)
+		{
+			ANKI_GLTF_LOGE(
+				"Bone weights count is incorrect. Is %u, should be %u", submesh.m_boneWeights.getSize(), vertCount);
+			return Error::USER_DATA;
+		}
+
 		//
 		//
-		// Fix normals. If normal A and normal B have the same position then merge them
+		// Fix normals. If normal A and normal B have the same position then try to merge them
 		//
 		//
 		for(U v = 0; v < vertCount; ++v)
 		for(U v = 0; v < vertCount; ++v)
 		{
 		{
@@ -426,13 +468,27 @@ Error Importer::writeMesh(const cgltf_mesh& mesh)
 			uva.m_format = Format::R16G16_SFLOAT;
 			uva.m_format = Format::R16G16_SFLOAT;
 		}
 		}
 		uva.m_relativeOffset = sizeof(U32) * 2;
 		uva.m_relativeOffset = sizeof(U32) * 2;
-		uva.m_scale = 1.0;
+		uva.m_scale = 1.0f;
+
+		// Bone weight
+		if(hasBoneWeights)
+		{
+			MeshBinaryFile::VertexAttribute& bidxa = header.m_vertexAttributes[VertexAttributeLocation::BONE_INDICES];
+			bidxa.m_bufferBinding = 2;
+			bidxa.m_format = Format::R16G16B16A16_UINT;
+			bidxa.m_relativeOffset = 0;
+			bidxa.m_scale = 1.0f;
+
+			MeshBinaryFile::VertexAttribute& wa = header.m_vertexAttributes[VertexAttributeLocation::BONE_WEIGHTS];
+			wa.m_bufferBinding = 2;
+			wa.m_format = Format::R8G8B8A8_UNORM;
+			wa.m_relativeOffset = sizeof(U16Vec4);
+			wa.m_scale = 1.0f;
+		}
 	}
 	}
 
 
 	// Arange the attributes into vert buffers
 	// Arange the attributes into vert buffers
 	{
 	{
-		header.m_vertexBufferCount = 2;
-
 		// First buff has positions
 		// First buff has positions
 		const MeshBinaryFile::VertexAttribute& posa = header.m_vertexAttributes[VertexAttributeLocation::POSITION];
 		const MeshBinaryFile::VertexAttribute& posa = header.m_vertexAttributes[VertexAttributeLocation::POSITION];
 		if(posa.m_format == Format::R32G32B32_SFLOAT)
 		if(posa.m_format == Format::R32G32B32_SFLOAT)
@@ -447,9 +503,18 @@ Error Importer::writeMesh(const cgltf_mesh& mesh)
 		{
 		{
 			ANKI_ASSERT(0);
 			ANKI_ASSERT(0);
 		}
 		}
+		++header.m_vertexBufferCount;
 
 
 		// 2nd buff has normal + tangent + texcoords
 		// 2nd buff has normal + tangent + texcoords
 		header.m_vertexBuffers[1].m_vertexStride = sizeof(U32) * 2 + sizeof(U16) * 2;
 		header.m_vertexBuffers[1].m_vertexStride = sizeof(U32) * 2 + sizeof(U16) * 2;
+		++header.m_vertexBufferCount;
+
+		// 3rd has bone weights
+		if(hasBoneWeights)
+		{
+			header.m_vertexBuffers[2].m_vertexStride = sizeof(WeightVertex);
+			++header.m_vertexBufferCount;
+		}
 	}
 	}
 
 
 	// Write some other header stuff
 	// Write some other header stuff
@@ -571,6 +636,31 @@ Error Importer::writeMesh(const cgltf_mesh& mesh)
 		ANKI_CHECK(file.write(&verts[0], verts.getSizeInBytes()));
 		ANKI_CHECK(file.write(&verts[0], verts.getSizeInBytes()));
 	}
 	}
 
 
+	// Write 3rd vert buffer
+	if(hasBoneWeights)
+	{
+		for(const SubMesh& submesh : submeshes)
+		{
+			DynamicArrayAuto<WeightVertex> verts(m_alloc);
+			verts.create(submesh.m_boneIds.getSize());
+
+			for(U i = 0; i < verts.getSize(); ++i)
+			{
+				WeightVertex vert;
+
+				for(U c = 0; c < 4; ++c)
+				{
+					vert.m_boneIndices[c] = submesh.m_boneIds[i][c];
+					vert.m_weights[c] = submesh.m_boneWeights[i][c];
+				}
+
+				verts[i] = vert;
+			}
+
+			ANKI_CHECK(file.write(&verts[0], verts.getSizeInBytes()));
+		}
+	}
+
 	return Error::NONE;
 	return Error::NONE;
 }
 }