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). Vector3F position = particleSystemData.Pose.Position; // Position in world space. position = node.PoseWorld.ToWorldPosition(position); // Determine distance to camera. Vector3F cameraToNode = position - _cameraPose.Position; // Planar distance: Project vector onto look direction. distance = Vector3F.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 Vector3F scale, ref Pose pose, ref Vector3F 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. Vector3F scale = Vector3F.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. Vector3F 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 Vector3F scale, ref Pose pose, ref Vector3F 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? Vector3F?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++; Vector3F nextPosition; if (requiresTransformation) { nextPosition = pose.ToWorldPosition(particles[index].Position * scale); } else { nextPosition = particles[index].Position; } Vector3F normal; if (uniformNormal.HasValue) { // Uniform normal. normal = uniformNormal.Value; } else { // Varying normal. normal = particles[index].Normal; if (requiresTransformation) { normal = pose.ToWorldDirection(normal); } } Vector3F previousDelta = nextPosition - p0.Position; p0.Axis = Vector3F.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); } } Vector3F delta = nextPosition - p1.Position; Vector3F tangent = delta + previousDelta; // Note: Should we normalize vectors for better average? p1.Axis = Vector3F.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 = Vector3F.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 Vector3F scale, ref Pose pose, ref Vector3F 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 Vector3F scale, ref Pose pose, ref Vector3F 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. Vector3F cameraToParticle = particleIndex.Position - _cameraPose.Position; particleIndex.Distance = Vector3F.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); } }
private void SynchronizeNested(List<ParticleSystemData> nestedData, ParticleSystem particleSystem) { // Render data is cached in ParticleSystem.RenderData. var renderData = particleSystem.RenderData as ParticleSystemData; if (renderData == null) { renderData = new ParticleSystemData(particleSystem); particleSystem.RenderData = renderData; } // Note: renderData.Frame is set in root particle system. It is not necessary // to check the frame number for nested particle systems. renderData.Update(particleSystem); nestedData.Add(renderData); // Synchronize nested particle systems. if (particleSystem.Children != null) foreach (var child in particleSystem.Children) SynchronizeNested(nestedData, child); }
/// <summary> /// Synchronizes the graphics data with the particle system data. (Needs to be called once per /// frame!) /// </summary> /// <param name="graphicsService">The graphics service.</param> /// <remarks> /// This method needs to be called once per frame to synchronize the graphics service with the /// particle system service. It creates a snapshot of the particle system and converts the /// particles to render data. /// </remarks> /// <exception cref="ArgumentNullException"> /// <paramref name="graphicsService"/> is <see langword="null"/>. /// </exception> public void Synchronize(IGraphicsService graphicsService) { if (graphicsService == null) throw new ArgumentNullException("graphicsService"); int frame = graphicsService.Frame + 1; SynchronizeShape(); // ----- Synchronize pose. if (ParticleSystem.ReferenceFrame == ParticleReferenceFrame.World) { PoseWorld = ParticleSystem.Pose; // Note: Scale is ignored and not copied. Because we cannot simply set ScaleWorld. // ParticleSystems with reference frame World should not use a scale. } // If the ReferenceFrame is Local, then ParticleSystem.Pose is irrelevant. // (The particle system can be instanced. Particles are relative to the scene // node.) // ----- Synchronize render data. // Render data is cached in ParticleSystem.RenderData. var renderData = ParticleSystem.RenderData as ParticleSystemData; if (renderData == null) { renderData = new ParticleSystemData(ParticleSystem); ParticleSystem.RenderData = renderData; } else if (renderData.Frame == frame) { // Render data is up-to-date. return; } else if (!ParticleSystem.Enabled && renderData.Frame == frame - 1) { // The particle system was updated in the last frame and the particles haven't // changed since. (The particle system is disabled.) renderData.Frame = frame; return; } // Synchronize render data of root particle system. renderData.Update(ParticleSystem); // Synchronize render data of nested particle systems. if (ParticleSystem.Children != null && ParticleSystem.Children.Count > 0) { if (renderData.NestedRenderData == null) renderData.NestedRenderData = new List<ParticleSystemData>(); else renderData.NestedRenderData.Clear(); foreach (var childParticleSystem in ParticleSystem.Children) SynchronizeNested(renderData.NestedRenderData, childParticleSystem); } else { // Clear nested render data. renderData.NestedRenderData = null; } renderData.Frame = frame; }