private void AddJob(ParticleSystemNode node, ParticleSystemData particleSystemData, bool sortByDistance, bool backToFront) { if (particleSystemData.Particles.Count == 0) { return; } float distance = 0; if (sortByDistance) { // Position relative to ParticleSystemNode (root particle system). Vector3 position = particleSystemData.Pose.Position; // Position in world space. position = node.PoseWorld.ToWorldPosition(position); // Determine distance to camera. Vector3 cameraToNode = position - _cameraPose.Position; // Planar distance: Project vector onto look direction. distance = Vector3.Dot(cameraToNode, _cameraForward); // Use linear distance for viewpoint-oriented and world-oriented billboards. if (particleSystemData.BillboardOrientation.Normal != BillboardNormal.ViewPlaneAligned) { distance = cameraToNode.LengthSquared() * Math.Sign(distance); } if (backToFront) { distance = -distance; } } // Add draw job to list. ushort drawOrder = (ushort)particleSystemData.DrawOrder; var textureId = GetTextureId(particleSystemData.Texture); var job = new Job { SortKey = GetSortKey(distance, drawOrder, textureId), Node = node, ParticleSystemData = particleSystemData, }; _jobs.Add(ref job); }
private void DrawParticlesOldToNew(ParticleSystemData particleSystemData, bool requiresTransformation, ref Vector3 scale, ref Pose pose, ref Vector3 color, float alpha, float angleOffset) { var b = new BillboardArgs { Orientation = particleSystemData.BillboardOrientation, Softness = particleSystemData.Softness, ReferenceAlpha = particleSystemData.AlphaTest, }; int numberOfParticles = particleSystemData.Particles.Count; var particles = particleSystemData.Particles.Array; bool isViewPlaneAligned = (particleSystemData.BillboardOrientation.Normal == BillboardNormal.ViewPlaneAligned); bool isAxisInViewSpace = particleSystemData.BillboardOrientation.IsAxisInViewSpace; for (int i = 0; i < numberOfParticles; i++) { if (particles[i].IsAlive) // Skip dead particles. { if (requiresTransformation) { b.Position = pose.ToWorldPosition(particles[i].Position * scale); b.Normal = isViewPlaneAligned ? _defaultNormal : pose.ToWorldDirection(particles[i].Normal); b.Axis = isAxisInViewSpace ? particles[i].Axis : pose.ToWorldDirection(particles[i].Axis); b.Size = particles[i].Size * scale.Y; // Assume uniform scale for size. } else { b.Position = particles[i].Position; b.Normal = isViewPlaneAligned ? _defaultNormal : particles[i].Normal; b.Axis = particles[i].Axis; b.Size = particles[i].Size; } b.Angle = particles[i].Angle + angleOffset; b.Color = particles[i].Color * color; b.Alpha = particles[i].Alpha * alpha; b.AnimationTime = particles[i].AnimationTime; b.BlendMode = particles[i].BlendMode; var texture = particleSystemData.Texture ?? _debugTexture; _billboardBatch.DrawBillboard(ref b, texture); } } }
private void Draw(ParticleSystemNode node, ParticleSystemData particleSystemData) { // Scale and pose. Vector3 scale = Vector3.One; Pose pose = Pose.Identity; bool requiresTransformation = (particleSystemData.ReferenceFrame == ParticleReferenceFrame.Local); if (requiresTransformation) { scale = node.ScaleWorld; pose = node.PoseWorld * particleSystemData.Pose; } // Tint color and alpha. Vector3 color = node.Color; float alpha = node.Alpha; float angleOffset = node.AngleOffset; if (particleSystemData.IsRibbon) { if (particleSystemData.AxisParameter == null) { // Ribbons with automatic axis. DrawParticleRibbonsAuto(particleSystemData, requiresTransformation, ref scale, ref pose, ref color, alpha); } else { // Ribbons with fixed axis. DrawParticleRibbonsFixed(particleSystemData, requiresTransformation, ref scale, ref pose, ref color, alpha); } } else if (particleSystemData.IsDepthSorted) { // Particles sorted by depth. DrawParticlesBackToFront(particleSystemData, requiresTransformation, ref scale, ref pose, ref color, alpha, angleOffset); } else { // Particles sorted by age. DrawParticlesOldToNew(particleSystemData, requiresTransformation, ref scale, ref pose, ref color, alpha, angleOffset); } }
private void DrawParticleRibbonsAuto(ParticleSystemData particleSystemData, bool requiresTransformation, ref Vector3 scale, ref Pose pose, ref Vector3 color, float alpha) { // At least two particles are required to create a ribbon. int numberOfParticles = particleSystemData.Particles.Count; if (numberOfParticles < 2) { return; } // The up axis is not defined and needs to be derived automatically: // - Compute tangents along the ribbon curve. // - Build cross-products of normal and tangent vectors. // Is normal uniform across all particles? Vector3?uniformNormal; switch (particleSystemData.BillboardOrientation.Normal) { case BillboardNormal.ViewPlaneAligned: uniformNormal = _defaultNormal; break; case BillboardNormal.ViewpointOriented: uniformNormal = _cameraPose.Position - pose.Position; if (!uniformNormal.Value.TryNormalize()) { uniformNormal = _defaultNormal; } break; default: var normalParameter = particleSystemData.NormalParameter; if (normalParameter == null) { uniformNormal = _defaultNormal; } else if (normalParameter.IsUniform) { uniformNormal = normalParameter.DefaultValue; if (requiresTransformation) { uniformNormal = pose.ToWorldDirection(uniformNormal.Value); } } else { // Normal is set in particle data. uniformNormal = null; } break; } var texture = particleSystemData.Texture ?? _debugTexture; var particles = particleSystemData.Particles.Array; int index = 0; do { // ----- Skip dead particles. while (index < numberOfParticles && !particles[index].IsAlive) { index++; } // ----- Start of new ribbon. int endIndex = index + 1; while (endIndex < numberOfParticles && particles[endIndex].IsAlive) { endIndex++; } int numberOfSegments = endIndex - index - 1; var p0 = new RibbonArgs { // Uniform parameters Softness = particleSystemData.Softness, ReferenceAlpha = particleSystemData.AlphaTest }; var p1 = new RibbonArgs { // Uniform parameters Softness = particleSystemData.Softness, ReferenceAlpha = particleSystemData.AlphaTest }; // Compute axes and render ribbon. // First particle. if (requiresTransformation) { p0.Position = pose.ToWorldPosition(particles[index].Position * scale); p0.Size = particles[index].Size.Y * scale.Y; } else { p0.Position = particles[index].Position; p0.Size = particles[index].Size.Y; } p0.Color = particles[index].Color * color; p0.Alpha = particles[index].Alpha * alpha; p0.AnimationTime = particles[index].AnimationTime; p0.BlendMode = particles[index].BlendMode; p0.TextureCoordinateU = 0; index++; Vector3 nextPosition; if (requiresTransformation) { nextPosition = pose.ToWorldPosition(particles[index].Position * scale); } else { nextPosition = particles[index].Position; } Vector3 normal; if (uniformNormal.HasValue) { // Uniform normal. normal = uniformNormal.Value; } else { // Varying normal. normal = particles[index].Normal; if (requiresTransformation) { normal = pose.ToWorldDirection(normal); } } Vector3 previousDelta = nextPosition - p0.Position; p0.Axis = Vector3.Cross(normal, previousDelta); p0.Axis.TryNormalize(); // Intermediate particles. while (index < endIndex - 1) { p1.Position = nextPosition; if (requiresTransformation) { nextPosition = pose.ToWorldPosition(particles[index + 1].Position * scale); p1.Size = particles[index].Size.Y * scale.Y; } else { nextPosition = particles[index + 1].Position; p1.Size = particles[index].Size.Y; } if (uniformNormal.HasValue) { // Uniform normal. normal = uniformNormal.Value; } else { // Varying normal. normal = particles[index].Normal; if (requiresTransformation) { normal = pose.ToWorldDirection(normal); } } Vector3 delta = nextPosition - p1.Position; Vector3 tangent = delta + previousDelta; // Note: Should we normalize vectors for better average? p1.Axis = Vector3.Cross(normal, tangent); p1.Axis.TryNormalize(); p1.Color = particles[index].Color * color; p1.Alpha = particles[index].Alpha * alpha; p1.AnimationTime = particles[index].AnimationTime; p1.BlendMode = particles[index].BlendMode; p1.TextureCoordinateU = GetTextureCoordinateU1(index - 1, numberOfSegments, particleSystemData.TextureTiling); // Draw ribbon segment. _billboardBatch.DrawRibbon(ref p0, ref p1, texture); p0 = p1; p0.TextureCoordinateU = GetTextureCoordinateU0(index, numberOfSegments, particleSystemData.TextureTiling); previousDelta = delta; index++; } // Last particle. p1.Position = nextPosition; if (uniformNormal.HasValue) { // Uniform normal. normal = uniformNormal.Value; } else { // Varying normal. normal = particles[index].Normal; if (requiresTransformation) { normal = pose.ToWorldDirection(normal); } } p1.Axis = Vector3.Cross(normal, previousDelta); p1.Axis.TryNormalize(); if (requiresTransformation) { p1.Size = particles[index].Size.Y * scale.Y; } else { p1.Size = particles[index].Size.Y; } p1.Color = particles[index].Color * color; p1.Alpha = particles[index].Alpha * alpha; p1.AnimationTime = particles[index].AnimationTime; p1.BlendMode = particles[index].BlendMode; p1.TextureCoordinateU = GetTextureCoordinateU1(index - 1, numberOfSegments, particleSystemData.TextureTiling); // Draw last ribbon segment. _billboardBatch.DrawRibbon(ref p0, ref p1, texture); index++; } while (index < numberOfParticles); }
// Particle ribbons: // Particles can be rendered as ribbons (a.k.a. trails, lines). Subsequent living // particles are connected using rectangles. // +--------------+--------------+ // | | | // p0 p1 p2 // | | | // +--------------+--------------+ // At least two living particles are required to create a ribbon. Dead particles // ("NormalizedAge" ≥ 1) can be used as delimiters to terminate one ribbon and // start the next ribbon. // // p0 and p1 can have different colors and alpha values to create color gradients // or a ribbon that fades in/out. private void DrawParticleRibbonsFixed(ParticleSystemData particleSystemData, bool requiresTransformation, ref Vector3 scale, ref Pose pose, ref Vector3 color, float alpha) { // At least two particles are required to create a ribbon. int numberOfParticles = particleSystemData.Particles.Count; if (numberOfParticles < 2) { return; } var particles = particleSystemData.Particles.Array; bool isAxisInViewSpace = particleSystemData.BillboardOrientation.IsAxisInViewSpace; int index = 0; do { // ----- Skip dead particles. while (index < numberOfParticles && !particles[index].IsAlive) { index++; } // ----- Start of new ribbon. int endIndex = index + 1; while (endIndex < numberOfParticles && particles[endIndex].IsAlive) { endIndex++; } int numberOfSegments = endIndex - index - 1; var p0 = new RibbonArgs { // Uniform parameters Softness = particleSystemData.Softness, ReferenceAlpha = particleSystemData.AlphaTest }; var p1 = new RibbonArgs { // Uniform parameters Softness = particleSystemData.Softness, ReferenceAlpha = particleSystemData.AlphaTest }; p0.Axis = particles[index].Axis; if (requiresTransformation) { p0.Position = pose.ToWorldPosition(particles[index].Position * scale); if (!isAxisInViewSpace) { p0.Axis = pose.ToWorldDirection(p0.Axis); } p0.Size = particles[index].Size.Y * scale.Y; } else { p0.Position = particles[index].Position; p0.Size = particles[index].Size.Y; } p0.Color = particles[index].Color * color; p0.Alpha = particles[index].Alpha * alpha; p0.AnimationTime = particles[index].AnimationTime; p0.BlendMode = particles[index].BlendMode; p0.TextureCoordinateU = 0; index++; while (index < endIndex) { p1.Axis = particles[index].Axis; if (requiresTransformation) { p1.Position = pose.ToWorldPosition(particles[index].Position * scale); if (!isAxisInViewSpace) { p1.Axis = pose.ToWorldDirection(p1.Axis); } p1.Size = particles[index].Size.Y * scale.Y; } else { p1.Position = particles[index].Position; p1.Size = particles[index].Size.Y; } p1.Color = particles[index].Color * color; p1.Alpha = particles[index].Alpha * alpha; p1.AnimationTime = particles[index].AnimationTime; p1.BlendMode = particles[index].BlendMode; p1.TextureCoordinateU = GetTextureCoordinateU1(index - 1, numberOfSegments, particleSystemData.TextureTiling); // Draw ribbon segment. var texture = particleSystemData.Texture ?? _debugTexture; _billboardBatch.DrawRibbon(ref p0, ref p1, texture); p0 = p1; p0.TextureCoordinateU = GetTextureCoordinateU0(index, numberOfSegments, particleSystemData.TextureTiling); index++; } } while (index < numberOfParticles); }
private void DrawParticlesBackToFront(ParticleSystemData particleSystemData, bool requiresTransformation, ref Vector3 scale, ref Pose pose, ref Vector3 color, float alpha, float angleOffset) { var b = new BillboardArgs { Orientation = particleSystemData.BillboardOrientation, Softness = particleSystemData.Softness, ReferenceAlpha = particleSystemData.AlphaTest, }; int numberOfParticles = particleSystemData.Particles.Count; var particles = particleSystemData.Particles.Array; if (_particleIndices == null) { _particleIndices = new ArrayList <ParticleIndex>(numberOfParticles); } else { _particleIndices.Clear(); _particleIndices.EnsureCapacity(numberOfParticles); } // Use linear distance for viewpoint-oriented and world-oriented billboards. bool useLinearDistance = (particleSystemData.BillboardOrientation.Normal != BillboardNormal.ViewPlaneAligned); // Compute positions and distance to camera. for (int i = 0; i < numberOfParticles; i++) { if (particles[i].IsAlive) // Skip dead particles. { var particleIndex = new ParticleIndex(); particleIndex.Index = i; if (requiresTransformation) { particleIndex.Position = pose.ToWorldPosition(particles[i].Position * scale); } else { particleIndex.Position = particles[i].Position; } // Planar distance: Project vector onto look direction. Vector3 cameraToParticle = particleIndex.Position - _cameraPose.Position; particleIndex.Distance = Vector3.Dot(cameraToParticle, _cameraForward); if (useLinearDistance) { particleIndex.Distance = cameraToParticle.Length * Math.Sign(particleIndex.Distance); } _particleIndices.Add(ref particleIndex); } } // Sort particles back-to-front. _particleIndices.Sort(ParticleIndexComparer.Instance); bool isViewPlaneAligned = (particleSystemData.BillboardOrientation.Normal == BillboardNormal.ViewPlaneAligned); bool isAxisInViewSpace = particleSystemData.BillboardOrientation.IsAxisInViewSpace; // Draw sorted particles. var indices = _particleIndices.Array; numberOfParticles = _particleIndices.Count; // Dead particles have been removed. for (int i = 0; i < numberOfParticles; i++) { int index = indices[i].Index; b.Position = indices[i].Position; if (requiresTransformation) { b.Normal = isViewPlaneAligned ? _defaultNormal : pose.ToWorldDirection(particles[index].Normal); b.Axis = isAxisInViewSpace ? particles[index].Axis : pose.ToWorldDirection(particles[index].Axis); b.Size = particles[index].Size * scale.Y; // Assume uniform scale for size. } else { b.Normal = isViewPlaneAligned ? _defaultNormal : particles[index].Normal; b.Axis = particles[index].Axis; b.Size = particles[index].Size; } b.Angle = particles[index].Angle + angleOffset; b.Color = particles[index].Color * color; b.Alpha = particles[index].Alpha * alpha; b.AnimationTime = particles[index].AnimationTime; b.BlendMode = particles[index].BlendMode; var texture = particleSystemData.Texture ?? _debugTexture; _billboardBatch.DrawBillboard(ref b, texture); } }