Browse Source

More work

Panagiotis Christopoulos Charitos 6 years ago
parent
commit
77bb958809

+ 32 - 91
shaders/GpuParticles.glslp

@@ -3,114 +3,55 @@
 // Code licensed under the BSD License.
 // http://www.anki3d.org/LICENSE
 
-// This shader does a particle simulation
+#pragma anki input const Vec3 diffColor
+#pragma anki input const Vec3 specColor
+#pragma anki input const F32 roughness
+#pragma anki input const F32 metallic
+#pragma anki input const Vec3 emission
 
-#pragma anki input const U32 WORKGROUP_SIZE_X
-
-#pragma anki start comp
-
-#include <shaders/glsl_cpp_common/GpuParticles.h>
-#include <shaders/Common.glsl>
-
-layout(local_size_x = WORKGROUP_SIZE_X, local_size_y = 1, local_size_z = 1) in;
-
-layout(set = 0, binding = 0) uniform texture2D u_depthMap;
-
-layout(set = 1, binding = 0) buffer ssbo_
+layout(push_constant, row_major) pc_
 {
-	GpuParticle u_particles[];
+	Mat4 u_mvp;
+	Vec3 u_minusCameraZ;
+	U32 u_particleCount;
 };
 
-layout(set = 1, binding = 1) buffer ubo_
+layout(set = 0, binding = 0) buffer ssbo_
 {
-	GpuParticleEmitterProperties u_props;
-};
-
-layout(set = 1, binding = 2) uniform texture2D u_noiseTex;
-layout(set = 1, binding = 3) uniform sampler u_nearestAnyRepeatSampler;
-
-layout(std140, push_constant) pc_
-{
-	Mat4 u_viewProjMat;
-	Vec2 u_randomUv;
-	F32 u_dt;
-	F32 u_padding0;
-};
-
-const F32 noiseTexSizeX = F32(textureSize(u_noiseTex).x);
-
-F32 genRandomFactor()
-{
-	const Vec2 uv = u_randomUv + Vec2(gl_GlobalInvocationID.x) / noiseTexSizeX;
-	const F32 r = textureLod(u_noiseTex, u_nearestAnyRepeatSampler, uv).r;
-	return saturate(r);
+	GpuParticle u_particles[];
 }
 
-void initParticle(out GpuParticle p)
-{
-	const F32 randFactor = genRandomFactor();
-
-	p.m_newWorldPosition = mix(u_props.m_minStartingPosition, u_props.m_maxStartingPosition, randFactor);
-	p.m_oldWorldPosition = p.m_newWorldPosition;
+#pragma anki start vert
 
-	p.m_mass = mix(u_props.m_minMass, u_props.m_maxMass, randFactor);
-	p.m_life = mix(u_props.m_minLife, u_props.m_maxLife, randFactor);
-	p.m_acceleration = mix(u_props.m_minGravity, u_props.m_maxGravity, randFactor);
+#include <shaders/glsl_cpp_common/GpuParticles.h>
 
-	// Calculate the initial velocity
-	const Vec3 initialForce = mix(u_props.m_minForce, u_props.m_maxForce, randFactor);
-	const Vec3 totalForce = (p.m_acceleration * p.m_mass) + initialForce;
-	const Vec3 acceleration = totalForce / p.m_mass;
-	p.m_velocity = acceleration * u_dt;
-}
+layout(location = 0) flat out Vec2 out_velocity;
 
 void main()
 {
-	const u32 particleIdx = gl_GlobalInvocationID.x;
-	if(particleIdx >= u_particleCount)
-	{
-		return;
-	}
-
-	GpuParticle particle = u_particles[particleIdx];
-	const F32 dt = u_dt;
+	const Particle part = u_particles[gl_VertexID / 2];
 
-	// Check if it's dead
-	if(particle.m_life - dt <= 0.0)
-	{
-		// Dead, revive
-		initParticle(particle);
-	}
-	else
-	{
-		// Simulate
+	const Vec4 clipPos = u_mvp * Vec4(part.m_newWorldPosition, 1.0);
+	const Vec4 prevClipPos = u_mvp * Vec4(part.m_oldWorldPosition, 1.0);
 
-		const Vec3 xp = particle.m_oldWorldPosition;
-		const Vec3 xc = particle.m_acceleration * (u_dt * u_dt) + u_particles[particleIdx].m_velocity * u_dt + xp;
+	gl_Position = ((gl_VertexID & 1) == 0) ? clipPos : prevClipPos;
 
-		// Project the point
-		const Vec4 proj4 = u_viewProjMat * Vec4(xc, 1.0);
-		const Vec3 proj3 = proj4.xyz / proj4.w;
-		if(proj3.xy > Vec2(-1.0) && proj3.xy < Vec2(1.0))
-		{
-			// It's visible, test against the depth buffer
+	const Vec2 crntNdc = clipPos.xy / clipPos.w;
+	const Vec2 prevNdc = prevClipPos.xy / prevClipPos.w;
 
-			const F32 refDepth = textureLod(u_depthMap, u_nearestAnyRepeatSampler, NDC_TO_UV(proj3.xy), 0.0).r;
-			const F32 testDepth = proj3.z;
+	// It's NDC_TO_UV(prevNdc) - NDC_TO_UV(crntNdc) or:
+	out_velocity = (prevNdc - crntNdc) * 0.5;
+}
+#pragma anki end
 
-			if(testDepth >= refDepth)
-			{
-				// Collides, change its direction
-				p.m_velocity = -p.m_velocity;
-			}
-		}
+#pragma anki start frag
+#include <shaders/GBufferCommonFrag.glsl>
 
-		particle.m_oldWorldPosition = particle.m_newWorldPosition;
-		particle.m_newWorldPosition = xc;
-	}
+layout(location = 0) flat in Vec2 in_velocity;
 
-	// Write back the particle
-	u_particles[particleIdx] = particle;
+void main()
+{
+	const Vec3 normal = u_minusCameraZ;
+	writeRts(diffColor, normal, specColor, roughness, subsurface, emission, metallic, in_velocity);
 }
-
-#pragma anki end
+#pragma anki end

+ 116 - 0
shaders/GpuParticlesSimulation.glslp

@@ -0,0 +1,116 @@
+// Copyright (C) 2009-2019, Panagiotis Christopoulos Charitos and contributors.
+// All rights reserved.
+// Code licensed under the BSD License.
+// http://www.anki3d.org/LICENSE
+
+// This shader does a particle simulation
+
+#pragma anki input const U32 WORKGROUP_SIZE_X
+
+#pragma anki start comp
+
+#include <shaders/glsl_cpp_common/GpuParticles.h>
+#include <shaders/Common.glsl>
+
+layout(local_size_x = WORKGROUP_SIZE_X, local_size_y = 1, local_size_z = 1) in;
+
+layout(set = 0, binding = 0) uniform texture2D u_depthMap;
+
+layout(set = 1, binding = 0) buffer ssbo_
+{
+	GpuParticle u_particles[];
+};
+
+layout(set = 1, binding = 1) buffer ubo_
+{
+	GpuParticleEmitterProperties u_props;
+};
+
+layout(set = 1, binding = 2) readonly buffer ubo1_
+{
+	U32 u_randomFactorCount;
+	F32 u_randomFactors[];
+};
+
+layout(std430, push_constant, row_major) pc_
+{
+	GpuParticleSimulationState u_regs;
+};
+
+F32 readDepth(Vec2 uv)
+{
+	const Vec2 texSize = Vec2(textureSize(u_depthMap));
+	IVec2 textureSpaceUv = IVec2(uv * texSize);
+	textureSpaceUv = clamp(textureSpaceUv, IVec2(0), texSize - 1);
+	return texelFetch(u_depthMap, textureSpaceUv, 0).r;
+}
+
+void initParticle(out GpuParticle p)
+{
+	const F32 randFactor = u_randomFactors[(gl_GlobalInvocationID.x + u_regs.m_randomIndex) % u_randomFactorCount];
+
+	p.m_newWorldPosition =
+		mix(u_props.m_minStartingPosition, u_props.m_maxStartingPosition, randFactor) + u_regs.m_emitterPosition;
+	p.m_oldWorldPosition = p.m_newWorldPosition;
+
+	p.m_mass = mix(u_props.m_minMass, u_props.m_maxMass, randFactor);
+	p.m_life = mix(u_props.m_minLife, u_props.m_maxLife, randFactor);
+	p.m_acceleration = mix(u_props.m_minGravity, u_props.m_maxGravity, randFactor);
+
+	// Calculate the initial velocity
+	const Vec3 initialForce = mix(u_props.m_minForce, u_props.m_maxForce, randFactor);
+	const Vec3 totalForce = (p.m_acceleration * p.m_mass) + initialForce;
+	const Vec3 acceleration = totalForce / p.m_mass;
+	p.m_velocity = acceleration * u_regs.m_dt;
+}
+
+void main()
+{
+	const u32 particleIdx = gl_GlobalInvocationID.x;
+	if(particleIdx >= u_props.m_particleCount)
+	{
+		return;
+	}
+
+	GpuParticle particle = u_particles[particleIdx];
+	const F32 dt = u_regs.m_dt;
+
+	// Check if it's dead
+	if(particle.m_life - dt <= 0.0)
+	{
+		// Dead, revive
+		initParticle(particle);
+	}
+	else
+	{
+		// Simulate
+
+		const Vec3 xp = particle.m_oldWorldPosition;
+		const Vec3 xc = particle.m_acceleration * (dt * dt) + u_particles[particleIdx].m_velocity * dt + xp;
+
+		// Project the point
+		const Vec4 proj4 = u_regs.m_viewProjMat * Vec4(xc, 1.0);
+		const Vec3 proj3 = proj4.xyz / proj4.w;
+		if(proj3.xy > Vec2(-1.0) && proj3.xy < Vec2(1.0))
+		{
+			// It's visible, test against the depth buffer
+
+			const F32 refDepth = readDepth(NDC_TO_UV(proj3.xy));
+			const F32 testDepth = proj3.z;
+
+			if(testDepth >= refDepth)
+			{
+				// Collides, change its direction
+				p.m_velocity = -p.m_velocity;
+			}
+		}
+
+		particle.m_oldWorldPosition = particle.m_newWorldPosition;
+		particle.m_newWorldPosition = xc;
+	}
+
+	// Write back the particle
+	u_particles[particleIdx] = particle;
+}
+
+#pragma anki end

+ 14 - 2
shaders/glsl_cpp_common/GpuParticles.h

@@ -41,10 +41,22 @@ struct GpuParticle
 	F32 m_life;
 
 	Vec3 m_acceleration;
-	F32 m_padding1;
+	F32 m_padding0;
 
 	Vec3 m_velocity;
-	F32 m_padding2;
+	F32 m_padding1;
+};
+
+struct GpuParticleSimulationState
+{
+	Mat4 m_viewProjMat;
+
+	Vec2 m_padding0;
+	U32 m_randomIndex;
+	F32 m_dt;
+
+	Vec3 m_emitterPosition;
+	F32 m_padding1;
 };
 
 ANKI_END_NAMESPACE

+ 5 - 0
src/anki/renderer/GenericCompute.cpp

@@ -42,6 +42,11 @@ void GenericCompute::run(RenderPassWorkContext& rgraphCtx)
 	GenericGpuComputeJobQueueElementContext elementCtx;
 	elementCtx.m_commandBuffer = rgraphCtx.m_commandBuffer;
 	elementCtx.m_stagingGpuAllocator = &m_r->getStagingGpuMemoryManager();
+	elementCtx.m_viewMatrix = m_runCtx.m_ctx->m_matrices.m_view;
+	elementCtx.m_viewProjectionMatrix = m_runCtx.m_ctx->m_matrices.m_viewProjection;
+	elementCtx.m_projectionMatrix = m_runCtx.m_ctx->m_matrices.m_projection;
+	elementCtx.m_previousViewProjectionMatrix = m_runCtx.m_ctx->m_prevMatrices.m_viewProjection;
+	elementCtx.m_cameraTransform = m_runCtx.m_ctx->m_matrices.m_cameraTransform;
 
 	// Bind some state
 	rgraphCtx.bindTexture(0, 0, m_r->getDepthDownscale().getHiZRt(), TextureSubresourceInfo());

+ 5 - 1
src/anki/renderer/RenderQueue.h

@@ -55,7 +55,11 @@ class RenderableQueueElement final
 public:
 	RenderQueueDrawCallback m_callback;
 	const void* m_userData;
+
+	/// Elements with the same m_mergeKey and same m_callback may be merged and the m_callback will be called once.
+	/// Unless m_mergeKey is zero.
 	U64 m_mergeKey;
+
 	F32 m_distanceFromCamera; ///< Don't set this
 
 	RenderableQueueElement()
@@ -67,7 +71,7 @@ static_assert(
 	std::is_trivially_destructible<RenderableQueueElement>::value == true, "Should be trivially destructible");
 
 /// Context that contains variables for the GenericGpuComputeJobQueueElement.
-class GenericGpuComputeJobQueueElementContext final
+class GenericGpuComputeJobQueueElementContext final : public RenderingMatrices
 {
 public:
 	CommandBufferPtr m_commandBuffer;

+ 109 - 5
src/anki/scene/GpuParticleEmitterNode.cpp

@@ -8,6 +8,7 @@
 #include <anki/scene/components/MoveComponent.h>
 #include <anki/scene/components/SpatialComponent.h>
 #include <anki/scene/components/GenericGpuComputeJobComponent.h>
+#include <anki/scene/components/RenderComponent.h>
 #include <anki/resource/ResourceManager.h>
 #include <shaders/glsl_cpp_common/GpuParticles.h>
 
@@ -37,12 +38,37 @@ public:
 	}
 };
 
+class GpuParticleEmitterNode::MyRenderComponent : public MaterialRenderComponent
+{
+public:
+	MyRenderComponent(SceneNode* node)
+		: MaterialRenderComponent(node, static_cast<GpuParticleEmitterNode*>(node)->m_emitterRsrc->getMaterial())
+	{
+	}
+
+	void setupRenderableQueueElement(RenderableQueueElement& el) const override
+	{
+		static_cast<const GpuParticleEmitterNode&>(getSceneNode()).setupRenderableQueueElement(el);
+	}
+};
+
+GpuParticleEmitterNode::GpuParticleEmitterNode(SceneGraph* scene, CString name)
+	: SceneNode(scene, name)
+{
+}
+
+GpuParticleEmitterNode::~GpuParticleEmitterNode()
+{
+}
+
 Error GpuParticleEmitterNode::init(const CString& filename)
 {
 	// Create program
-	ANKI_CHECK(getResourceManager().loadResource("shaders/GpuParticles.glslp", m_prog));
+	ANKI_CHECK(getResourceManager().loadResource("shaders/GpuParticlesSimulation.glslp", m_prog));
 	const ShaderProgramResourceVariant* variant;
-	m_prog->getOrCreateVariant(variant);
+	ShaderProgramResourceConstantValueInitList<1> consts(m_prog);
+	consts.add("WORKGROUP_SIZE_X", U32(COMPUTE_SHADER_WORKGROUP_SIZE_X));
+	m_prog->getOrCreateVariant(consts.get(), variant);
 	m_grProg = variant->getProgram();
 
 	// Load particle props
@@ -70,11 +96,14 @@ Error GpuParticleEmitterNode::init(const CString& filename)
 
 	m_propsBuff->unmap();
 
+	m_particleCount = inProps.m_maxNumOfParticles;
+
 	// Create the particle buffer
 	buffInit.m_access = BufferMapAccessBit::WRITE;
 	buffInit.m_usage = BufferUsageBit::STORAGE_ALL;
 	buffInit.m_size = sizeof(GpuParticle) * inProps.m_maxNumOfParticles;
 	m_particlesBuff = getSceneGraph().getGrManager().newBuffer(buffInit);
+
 	GpuParticle* particle = static_cast<GpuParticle*>(m_particlesBuff->map(0, MAX_PTR_SIZE, BufferMapAccessBit::WRITE));
 	const GpuParticle* end = particle + inProps.m_maxNumOfParticles;
 	for(; particle < end; ++particle)
@@ -84,6 +113,23 @@ Error GpuParticleEmitterNode::init(const CString& filename)
 
 	m_particlesBuff->unmap();
 
+	// Create the rand buffer
+	buffInit.m_size = sizeof(U32) + MAX_RAND_FACTORS * sizeof(F32);
+	m_randFactorsBuff = getSceneGraph().getGrManager().newBuffer(buffInit);
+
+	F32* randFactors = static_cast<F32*>(m_randFactorsBuff->map(0, MAX_PTR_SIZE, BufferMapAccessBit::WRITE));
+
+	*reinterpret_cast<U32*>(randFactors) = MAX_RAND_FACTORS;
+	++randFactors;
+
+	const F32* randFactorsEnd = randFactors + MAX_RAND_FACTORS;
+	for(; randFactors < randFactorsEnd; ++randFactors)
+	{
+		*randFactors = randRange(0.0f, 1.0f);
+	}
+
+	m_randFactorsBuff->unmap();
+
 	// Find the extend of the particles
 	{
 		const Vec3 maxForce = inProps.m_particle.m_maxForceDirection * inProps.m_particle.m_maxForceMagnitude;
@@ -104,16 +150,74 @@ Error GpuParticleEmitterNode::init(const CString& filename)
 	GenericGpuComputeJobComponent* gpuComp = newComponent<GenericGpuComputeJobComponent>();
 	gpuComp->setCallback(
 		[](GenericGpuComputeJobQueueElementContext& ctx, const void* userData) {
-			static_cast<const GpuParticleEmitterNode*>(userData)->simulate(ctx.m_commandBuffer);
+			static_cast<const GpuParticleEmitterNode*>(userData)->simulate(ctx);
 		},
 		this);
 
 	return Error::NONE;
 }
 
-void GpuParticleEmitterNode::simulate(CommandBufferPtr& cmdb) const
+void GpuParticleEmitterNode::onMoveComponentUpdate(const MoveComponent& movec)
+{
+	const Vec4& pos = movec.getWorldTransform().getOrigin();
+
+	// Update the AABB
+	m_spatialVolume.setMin(pos - m_maxDistanceAParticleCanGo);
+	m_spatialVolume.setMax(pos + m_maxDistanceAParticleCanGo);
+	getComponent<SpatialComponent>().markForUpdate();
+
+	// Stash the position
+	m_worldPosition = pos.xyz();
+}
+
+void GpuParticleEmitterNode::simulate(GenericGpuComputeJobQueueElementContext& ctx) const
+{
+	CommandBufferPtr& cmdb = ctx.m_commandBuffer;
+
+	// Bind resources
+	cmdb->bindStorageBuffer(1, 0, m_particlesBuff, 0, MAX_PTR_SIZE);
+	cmdb->bindUniformBuffer(1, 1, m_propsBuff, 0, MAX_PTR_SIZE);
+	cmdb->bindStorageBuffer(1, 2, m_randFactorsBuff, 0, MAX_PTR_SIZE);
+
+	GpuParticleSimulationState pc;
+	pc.m_viewProjMat = ctx.m_viewProjectionMatrix;
+	pc.m_randomIndex = rand();
+	pc.m_dt = m_dt;
+	pc.m_emitterPosition = m_worldPosition;
+	cmdb->setPushConstants(&pc, sizeof(pc));
+
+	// Dispatch
+	const U workgroupCount = (m_particleCount + COMPUTE_SHADER_WORKGROUP_SIZE_X - 1) / COMPUTE_SHADER_WORKGROUP_SIZE_X;
+	cmdb->dispatchCompute(workgroupCount, 1, 1);
+}
+
+void GpuParticleEmitterNode::setupRenderableQueueElement(RenderableQueueElement& el) const
 {
-	// TODO
+	el.m_callback = [](RenderQueueDrawContext& ctx, ConstWeakArray<void*> userData) {
+		ANKI_ASSERT(userData.getSize() == 1);
+		static_cast<const GpuParticleEmitterNode*>(userData[0])->draw(ctx);
+	};
+	el.m_mergeKey = 0; // No merging
+	el.m_userData = this;
+}
+
+void GpuParticleEmitterNode::draw(RenderQueueDrawContext& ctx) const
+{
+	CommandBufferPtr& cmdb = ctx.m_commandBuffer;
+
+	if(!ctx.m_debugDraw)
+	{
+		// Program
+		ShaderProgramPtr prog;
+		m_emitterRsrc->getRenderingInfo(ctx.m_key.m_lod, prog);
+		cmdb->bindShaderProgram(prog);
+
+		// TODO
+	}
+	else
+	{
+		// TODO
+	}
 }
 
 } // end namespace anki

+ 23 - 2
src/anki/scene/GpuParticleEmitterNode.h

@@ -12,6 +12,11 @@
 namespace anki
 {
 
+// Forward
+class GenericGpuComputeJobQueueElementContext;
+class RenderableQueueElement;
+class RenderQueueDrawContext;
+
 /// @addtogroup scene
 /// @{
 
@@ -25,10 +30,18 @@ public:
 
 	ANKI_USE_RESULT Error init(const CString& filename);
 
-	ANKI_USE_RESULT Error frameUpdate(Second prevUpdateTime, Second crntTime) override;
+	ANKI_USE_RESULT Error frameUpdate(Second prevUpdateTime, Second crntTime) override
+	{
+		m_dt = crntTime - prevUpdateTime;
+		return Error::NONE;
+	}
 
 private:
+	static constexpr U COMPUTE_SHADER_WORKGROUP_SIZE_X = 64;
+	static constexpr U MAX_RAND_FACTORS = 32;
+
 	class MoveFeedbackComponent;
+	class MyRenderComponent;
 
 	ShaderProgramResourcePtr m_prog;
 	ShaderProgramPtr m_grProg;
@@ -37,13 +50,21 @@ private:
 
 	BufferPtr m_propsBuff; ///< Constant buffer with particle properties.
 	BufferPtr m_particlesBuff; ///< Particles buffer.
+	BufferPtr m_randFactorsBuff; ///< Contains flots with random values. Values in range [0.0, 1.0].
 
 	Aabb m_spatialVolume = Aabb(Vec3(-1.0f), Vec3(1.0f));
 	F32 m_maxDistanceAParticleCanGo = -1.0f;
+	U32 m_particleCount = 0;
+	F32 m_dt = 0.0f;
+	Vec3 m_worldPosition = Vec3(0.0f); //< Cache it.
 
 	void onMoveComponentUpdate(const MoveComponent& movec);
 
-	void simulate(CommandBufferPtr& cmdb) const;
+	void simulate(GenericGpuComputeJobQueueElementContext& ctx) const;
+
+	void setupRenderableQueueElement(RenderableQueueElement& el) const;
+
+	void draw(RenderQueueDrawContext& ctx) const;
 };
 /// @}
 

+ 139 - 0
src/anki/script/Scene.cpp

@@ -3474,6 +3474,75 @@ static inline void wrapParticleEmitterNode(lua_State* l)
 	lua_settop(l, 0);
 }
 
+LuaUserDataTypeInfo luaUserDataTypeInfoGpuParticleEmitterNode = {-3652396348144519688,
+	"GpuParticleEmitterNode",
+	LuaUserData::computeSizeForGarbageCollected<GpuParticleEmitterNode>(),
+	nullptr,
+	nullptr};
+
+template<>
+const LuaUserDataTypeInfo& LuaUserData::getDataTypeInfoFor<GpuParticleEmitterNode>()
+{
+	return luaUserDataTypeInfoGpuParticleEmitterNode;
+}
+
+/// Pre-wrap method GpuParticleEmitterNode::getSceneNodeBase.
+static inline int pwrapGpuParticleEmitterNodegetSceneNodeBase(lua_State* l)
+{
+	LuaUserData* ud;
+	(void)ud;
+	void* voidp;
+	(void)voidp;
+	PtrSize size;
+	(void)size;
+
+	if(ANKI_UNLIKELY(LuaBinder::checkArgsCount(l, 1)))
+	{
+		return -1;
+	}
+
+	// Get "this" as "self"
+	if(LuaBinder::checkUserData(l, 1, luaUserDataTypeInfoGpuParticleEmitterNode, ud))
+	{
+		return -1;
+	}
+
+	GpuParticleEmitterNode* self = ud->getData<GpuParticleEmitterNode>();
+
+	// Call the method
+	SceneNode& ret = *self;
+
+	// Push return value
+	voidp = lua_newuserdata(l, sizeof(LuaUserData));
+	ud = static_cast<LuaUserData*>(voidp);
+	luaL_setmetatable(l, "SceneNode");
+	extern LuaUserDataTypeInfo luaUserDataTypeInfoSceneNode;
+	ud->initPointed(&luaUserDataTypeInfoSceneNode, const_cast<SceneNode*>(&ret));
+
+	return 1;
+}
+
+/// Wrap method GpuParticleEmitterNode::getSceneNodeBase.
+static int wrapGpuParticleEmitterNodegetSceneNodeBase(lua_State* l)
+{
+	int res = pwrapGpuParticleEmitterNodegetSceneNodeBase(l);
+	if(res >= 0)
+	{
+		return res;
+	}
+
+	lua_error(l);
+	return 0;
+}
+
+/// Wrap class GpuParticleEmitterNode.
+static inline void wrapGpuParticleEmitterNode(lua_State* l)
+{
+	LuaBinder::createClass(l, &luaUserDataTypeInfoGpuParticleEmitterNode);
+	LuaBinder::pushLuaCFuncMethod(l, "getSceneNodeBase", wrapGpuParticleEmitterNodegetSceneNodeBase);
+	lua_settop(l, 0);
+}
+
 LuaUserDataTypeInfo luaUserDataTypeInfoReflectionProbeNode = {-801309373000950648,
 	"ReflectionProbeNode",
 	LuaUserData::computeSizeForGarbageCollected<ReflectionProbeNode>(),
@@ -4352,6 +4421,74 @@ static int wrapSceneGraphnewParticleEmitterNode(lua_State* l)
 	return 0;
 }
 
+/// Pre-wrap method SceneGraph::newGpuParticleEmitterNode.
+static inline int pwrapSceneGraphnewGpuParticleEmitterNode(lua_State* l)
+{
+	LuaUserData* ud;
+	(void)ud;
+	void* voidp;
+	(void)voidp;
+	PtrSize size;
+	(void)size;
+
+	if(ANKI_UNLIKELY(LuaBinder::checkArgsCount(l, 3)))
+	{
+		return -1;
+	}
+
+	// Get "this" as "self"
+	if(LuaBinder::checkUserData(l, 1, luaUserDataTypeInfoSceneGraph, ud))
+	{
+		return -1;
+	}
+
+	SceneGraph* self = ud->getData<SceneGraph>();
+
+	// Pop arguments
+	const char* arg0;
+	if(ANKI_UNLIKELY(LuaBinder::checkString(l, 2, arg0)))
+	{
+		return -1;
+	}
+
+	const char* arg1;
+	if(ANKI_UNLIKELY(LuaBinder::checkString(l, 3, arg1)))
+	{
+		return -1;
+	}
+
+	// Call the method
+	GpuParticleEmitterNode* ret = newSceneNode<GpuParticleEmitterNode>(self, arg0, arg1);
+
+	// Push return value
+	if(ANKI_UNLIKELY(ret == nullptr))
+	{
+		lua_pushstring(l, "Glue code returned nullptr");
+		return -1;
+	}
+
+	voidp = lua_newuserdata(l, sizeof(LuaUserData));
+	ud = static_cast<LuaUserData*>(voidp);
+	luaL_setmetatable(l, "GpuParticleEmitterNode");
+	extern LuaUserDataTypeInfo luaUserDataTypeInfoGpuParticleEmitterNode;
+	ud->initPointed(&luaUserDataTypeInfoGpuParticleEmitterNode, const_cast<GpuParticleEmitterNode*>(ret));
+
+	return 1;
+}
+
+/// Wrap method SceneGraph::newGpuParticleEmitterNode.
+static int wrapSceneGraphnewGpuParticleEmitterNode(lua_State* l)
+{
+	int res = pwrapSceneGraphnewGpuParticleEmitterNode(l);
+	if(res >= 0)
+	{
+		return res;
+	}
+
+	lua_error(l);
+	return 0;
+}
+
 /// Pre-wrap method SceneGraph::newReflectionProbeNode.
 static inline int pwrapSceneGraphnewReflectionProbeNode(lua_State* l)
 {
@@ -4755,6 +4892,7 @@ static inline void wrapSceneGraph(lua_State* l)
 	LuaBinder::pushLuaCFuncMethod(l, "newDirectionalLightNode", wrapSceneGraphnewDirectionalLightNode);
 	LuaBinder::pushLuaCFuncMethod(l, "newStaticCollisionNode", wrapSceneGraphnewStaticCollisionNode);
 	LuaBinder::pushLuaCFuncMethod(l, "newParticleEmitterNode", wrapSceneGraphnewParticleEmitterNode);
+	LuaBinder::pushLuaCFuncMethod(l, "newGpuParticleEmitterNode", wrapSceneGraphnewGpuParticleEmitterNode);
 	LuaBinder::pushLuaCFuncMethod(l, "newReflectionProbeNode", wrapSceneGraphnewReflectionProbeNode);
 	LuaBinder::pushLuaCFuncMethod(l, "newOccluderNode", wrapSceneGraphnewOccluderNode);
 	LuaBinder::pushLuaCFuncMethod(l, "newDecalNode", wrapSceneGraphnewDecalNode);
@@ -5168,6 +5306,7 @@ void wrapModuleScene(lua_State* l)
 	wrapDirectionalLightNode(l);
 	wrapStaticCollisionNode(l);
 	wrapParticleEmitterNode(l);
+	wrapGpuParticleEmitterNode(l);
 	wrapReflectionProbeNode(l);
 	wrapOccluderNode(l);
 	wrapDecalNode(l);

+ 16 - 0
src/anki/script/Scene.xml

@@ -389,6 +389,14 @@ using WeakArraySceneNodePtr = WeakArray<SceneNode*>;
 				</method>
 			</methods>
 		</class>
+		<class name="GpuParticleEmitterNode">
+			<methods>
+				<method name="getSceneNodeBase">
+					<overrideCall>SceneNode&amp; ret = *self;</overrideCall>
+					<return>SceneNode&amp;</return>
+				</method>
+			</methods>
+		</class>
 		<class name="ReflectionProbeNode">
 			<methods>
 				<method name="getSceneNodeBase">
@@ -492,6 +500,14 @@ using WeakArraySceneNodePtr = WeakArray<SceneNode*>;
 					</args>
 					<return>ParticleEmitterNode*</return>
 				</method>
+				<method name="newGpuParticleEmitterNode">
+					<overrideCall><![CDATA[GpuParticleEmitterNode* ret = newSceneNode<GpuParticleEmitterNode>(self, arg0, arg1);]]></overrideCall>
+					<args>
+						<arg>const CString&amp;</arg>
+						<arg>const CString&amp;</arg>
+					</args>
+					<return>GpuParticleEmitterNode*</return>
+				</method>
 				<method name="newReflectionProbeNode">
 					<overrideCall><![CDATA[ReflectionProbeNode* ret = newSceneNode<ReflectionProbeNode>(self, arg0, arg1, arg2);]]></overrideCall>
 					<args>

+ 16 - 5
tools/scene/Exporter.cpp

@@ -795,15 +795,20 @@ void Exporter::visitNode(const aiNode* ainode)
 		bool special = false;
 		GiProbe giProbe;
 		bool isGiProbe = false;
+		ParticleEmitter particleEmitter;
+		bool isParticleEmitter = false;
 		for(const auto& prop : m_scene->mMeshes[meshIndex]->mProperties)
 		{
 			if(prop.first == "particles")
 			{
-				ParticleEmitter p;
-				p.m_filename = prop.second;
-				p.m_transform = toAnkiMatrix(ainode->mTransformation);
-				m_particleEmitters.push_back(p);
+				particleEmitter.m_filename = prop.second;
+				particleEmitter.m_transform = toAnkiMatrix(ainode->mTransformation);
 				special = true;
+				isParticleEmitter = true;
+			}
+			else if(prop.first == "gpu_particles" && prop.second == "true")
+			{
+				particleEmitter.m_gpu = true;
 			}
 			else if(prop.first == "collision" && prop.second == "true")
 			{
@@ -952,6 +957,11 @@ void Exporter::visitNode(const aiNode* ainode)
 			m_giProbes.push_back(giProbe);
 		}
 
+		if(isParticleEmitter)
+		{
+			m_particleEmitters.push_back(particleEmitter);
+		}
+
 		if(special)
 		{
 			continue;
@@ -1039,7 +1049,8 @@ void Exporter::exportAll()
 	for(const ParticleEmitter& p : m_particleEmitters)
 	{
 		std::string name = "particles" + std::to_string(i);
-		file << "\nnode = scene:newParticleEmitterNode(\"" << name << "\", \"" << p.m_filename << "\")\n";
+		file << "\nnode = scene:new" << (p.m_gpu ? "Gpu" : "") << "ParticleEmitterNode(\"" << name << "\", \""
+			 << p.m_filename << "\")\n";
 
 		writeNodeTransform("node", p.m_transform);
 		++i;

+ 1 - 0
tools/scene/Exporter.h

@@ -55,6 +55,7 @@ class ParticleEmitter
 public:
 	std::string m_filename;
 	aiMatrix4x4 m_transform;
+	bool m_gpu = false;
 };
 
 class StaticCollisionNode