Skip to content

Commit 58de4c3

Browse files
committed
Allow for custom particle textures
1 parent 1527403 commit 58de4c3

File tree

9 files changed

+120
-21
lines changed

9 files changed

+120
-21
lines changed

Source/Documentation/Manual/physics.rst

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2471,6 +2471,9 @@ the one shown below::
24712471
ORTSMaxParticles ( 2500 )
24722472
ORTSRateMultiplier ( 1.0 )
24732473
ORTSUseChaoticRandomization ( false )
2474+
2475+
ORTSGraphic ( "smokemain.ace" )
2476+
ORTSGraphicAtlasLayout ( 4 4 )
24742477
)
24752478

24762479
The code block consists of the following elements:
@@ -2499,6 +2502,8 @@ The code block consists of the following elements:
24992502
single: ORTSMaxParticles
25002503
single: ORTSRateMultiplier
25012504
single: ORTSUseChaoticRandomization
2505+
single: ORTSGraphic
2506+
single: ORTSGraphicAtlasLayout
25022507

25032508
After including these settings, additional *optional* parameters unique to OR can
25042509
be included to further customize effect emitters:
@@ -2507,7 +2512,7 @@ be included to further customize effect emitters:
25072512
in the right/left, up/down, and front/back emission location of a particle (default units
25082513
are meters). Useful for non-circular exhaust ports, as it allows one particle emitter
25092514
to be used to spawn particles from an area, rather than a single point. Note that
2510-
``ORTSPositionVariation ( 1m 0m 0m )`` would allow particles to emit 1 meter right and
2515+
``ORTSPositionVariation ( x y z )`` would allow particles to emit 1 meter right and
25112516
1 meter left of the initial position, for a total variation of 2 meters. Similar is
25122517
true of all other parameters related to randomness, the total variation is double what's
25132518
specified. Feature is disabled by default.
@@ -2602,6 +2607,21 @@ be included to further customize effect emitters:
26022607
randomziation algorithm changes the random values by a small amount for each iteration. The "chaotic"
26032608
algorithm tends to make exhaust that is more spread out and discontinuous, which may be desireable in
26042609
some cases.
2610+
- ``ORTSGraphic ( "tex" )`` -- Gives the name and path to the texture that should be used to render
2611+
particles from this emitter. The default texture is "smokemain.ace" for steam-type emitters and
2612+
"dieselsmoke.ace" for diesel-type emitters. If the texture cannot be found from the engine's/wagon's
2613+
folder, then the ``GLOBAL\TEXTURES`` folder is checked, and if the texture is not there the ``Content``
2614+
folder included with OR is checked. Allowed texture formats are ``.png, .jpg, .bmp, .gif, .ace, or .dds``.
2615+
A path to a texture can also be used, such as ``ORTSGraphic ( "..\\SmokeTextures\\steam.dds" )``, to search
2616+
for textures not in the same folder as the engine or wagon.
2617+
- ``ORTSGraphicAtlasLayout ( w h )`` -- Particle textures generally include multiple sprites in a single file
2618+
to allow for randomization of each particle's appearance. In MSTS, this was a sprite atlas 4 wide and 4 high,
2619+
for a total of 16 variations on the particle graphic. When using custom particle textures, it may be desired
2620+
to use a custom sprite sheet that is not 4x4, in which case the atlas layout can be set by ``ORTSGraphicAtlasLayout``.
2621+
For example, a sprite sheet with 4 variations on the particle texture all in a row (4x1) can be represented by
2622+
``ORTSGraphicAtlasLayout ( 4 1 )``. Note that each sprite should be a perfect square as each particle is
2623+
rendered as a square. Rectangular textures will be stretched/squished. Do not change the atlas setting unless you
2624+
are certain of the texture used, as improper settings will be very aesthetically unpleasing.
26052625

26062626

26072627
.. index::
@@ -2621,7 +2641,7 @@ matter:
26212641
)
26222642

26232643
- ``ORTSPosition ( x y z )`` -- defines the width, height, length location of the emitter (in meters by default)
2624-
- ``ORTSInitialVelocity ( x y z )`` -- defines the right, up, forward components of emission direction
2644+
- ``ORTSInitialVelocity ( x y z )`` -- defines the (+/-) right/left, up/down, forward/backward components of emission direction
26252645
(unitless, the particle speed is multiplied by this vector to determine the 3D velocity of particles.
26262646
Speed can be divided by inserting values less than 1, or multiplied by inserting values greater than 1.)
26272647
- And ``ORTSParticleDiameter ( d )`` -- gives the nozzle width (in meters by default), which sets the initial

Source/Orts.Simulation/Simulation/RollingStocks/MSTSWagon.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4610,7 +4610,7 @@ public class ParticleEmitterData
46104610

46114611
public float SpeedLimitMpS = 150.0f;
46124612

4613-
public float NozzleDiameterM;
4613+
public float NozzleDiameterM = 0.1f;
46144614
public float NozzleAreaM2 = -1; // If left at -1, will be initialized later
46154615

46164616
public float RateFactor = 1.0f;
@@ -4628,6 +4628,10 @@ public class ParticleEmitterData
46284628

46294629
public bool ChaoticRandomization = false; // Changes the style of RNG used for particle motion
46304630

4631+
public string Graphic;
4632+
public int AtlasWidth = 4;
4633+
public int AtlasHeight = 4;
4634+
46314635
public ParticleEmitterData(STFReader stf)
46324636
{
46334637
stf.MustMatch("(");
@@ -4674,6 +4678,13 @@ public ParticleEmitterData(STFReader stf)
46744678
new STFReader.TokenProcessor("ortsmaxparticles", ()=>{ MaxParticles = stf.ReadIntBlock(null); }),
46754679
new STFReader.TokenProcessor("ortsratemultiplier", ()=>{ RateFactor = stf.ReadFloatBlock(STFReader.UNITS.None, null); }),
46764680
new STFReader.TokenProcessor("ortsusechaoticrandomization", ()=>{ ChaoticRandomization = stf.ReadBoolBlock(true); }),
4681+
new STFReader.TokenProcessor("ortsgraphic", ()=>{ Graphic = stf.ReadStringBlock(null); }),
4682+
new STFReader.TokenProcessor("ortsgraphicatlaslayout", ()=>{
4683+
stf.MustMatch("(");
4684+
AtlasWidth = Math.Max(stf.ReadInt(null), 1);
4685+
AtlasHeight = Math.Max(stf.ReadInt(null), 1);
4686+
stf.SkipRestOfBlock();
4687+
}),
46774688
});
46784689
}
46794690
}

Source/RunActivity/Content/ParticleEmitterShader.fx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,14 @@ float emitSize;
3333
float2 cameraTileXY;
3434
float currentTime;
3535

36-
static float2 texCoords[4] = { float2(0, 0), float2(0.25f, 0), float2(0.25f, 0.25f), float2(0, 0.25f) };
36+
static float2 texCoords[4] = { float2(0, 0), float2(1.0f, 0), float2(1.0f, 1.0f), float2(0, 1.0f) };
3737
static float3 offsets[4] = { float3(-0.5f, 0.5f, 0), float3(0.5f, 0.5f, 0), float3(0.5f, -0.5f, 0), float3(-0.5f, -0.5f, 0) };
3838

3939
float4 Fog;
4040

4141
// Textures
4242
texture particle_Tex;
43+
float2 texAtlasSize;
4344

4445
// Texture settings
4546
sampler ParticleSamp = sampler_state
@@ -142,9 +143,11 @@ VERTEX_OUTPUT VSParticles(in VERTEX_INPUT In)
142143

143144
Out.TexCoord = texCoords[vertIdx];
144145
float texAtlasPosition = In.TileXY_Vertex_ID.w;
145-
int atlasX = texAtlasPosition % 4;
146-
int atlasY = texAtlasPosition / 4;
147-
Out.TexCoord += float2(0.25f * atlasX, 0.25f * atlasY);
146+
int atlasX = texAtlasPosition % texAtlasSize.x;
147+
int atlasY = texAtlasPosition / texAtlasSize.y;
148+
Out.TexCoord.x /= texAtlasSize.x;
149+
Out.TexCoord.y /= texAtlasSize.y;
150+
Out.TexCoord += float2(atlasX / texAtlasSize.x, atlasY / texAtlasSize.y);
148151

149152
Out.Color_Age.rgb = In.Color_Random.rgb;
150153

Source/RunActivity/Viewer3D/Materials.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,14 @@ public Texture2D Get(string path, Texture2D defaultTexture, bool required = fals
6565
return defaultTexture;
6666

6767
path = path.ToLowerInvariant();
68+
var ext = Path.GetExtension(path);
69+
6870
if (!Textures.ContainsKey(path))
6971
{
7072
try
7173
{
7274
Texture2D texture;
73-
if (Path.GetExtension(path) == ".dds")
75+
if (ext == ".dds")
7476
{
7577
if (File.Exists(path))
7678
{
@@ -89,10 +91,10 @@ public Texture2D Get(string path, Texture2D defaultTexture, bool required = fals
8991
else return defaultTexture;
9092
}
9193
}
92-
else if (Path.GetExtension(path) == ".ace")
94+
else if (ext == ".ace")
9395
{
9496
var alternativeTexture = Path.ChangeExtension(path, ".dds");
95-
97+
9698
if (File.Exists(alternativeTexture))
9799
{
98100
DDSLib.DDSFromFile(alternativeTexture, GraphicsDevice, true, out texture);
@@ -145,7 +147,30 @@ Texture2D invalid()
145147
}
146148
}
147149
else
148-
return defaultTexture;
150+
{
151+
using (var stream = File.OpenRead(path))
152+
{
153+
if (ext == ".gif" || ext == ".jpg" || ext == ".png")
154+
texture = Texture2D.FromStream(GraphicsDevice, stream);
155+
else if (ext == ".bmp")
156+
{
157+
using (var image = System.Drawing.Image.FromStream(stream))
158+
{
159+
using (var memoryStream = new MemoryStream())
160+
{
161+
image.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
162+
memoryStream.Seek(0, SeekOrigin.Begin);
163+
texture = Texture2D.FromStream(GraphicsDevice, memoryStream);
164+
}
165+
}
166+
}
167+
else
168+
{
169+
Trace.TraceWarning("Unsupported texture format: {0}", path);
170+
return defaultTexture;
171+
}
172+
}
173+
}
149174

150175
Textures.Add(path, texture);
151176
return texture;

Source/RunActivity/Viewer3D/ParticleEmitter.cs

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222

2323
using System;
2424
using System.Collections.Generic;
25+
using System.Diagnostics;
26+
using System.IO;
2527
using Microsoft.Xna.Framework;
2628
using Microsoft.Xna.Framework.Graphics;
27-
using ORTS.Common;
2829
using Orts.Simulation.RollingStocks;
2930
using Orts.Viewer3D.RollingStock;
31+
using ORTS.Common;
3032

3133
namespace Orts.Viewer3D
3234
{
@@ -39,6 +41,7 @@ public class ParticleEmitterViewer
3941
public readonly float ParticleVolumeM3 = 0.001f;
4042
public readonly ParticleEmitterPrimitive Emitter;
4143

44+
public string TexturePath;
4245
ParticleEmitterMaterial Material;
4346

4447
#if DEBUG_EMITTER_INPUT
@@ -66,15 +69,40 @@ public ParticleEmitterViewer(Viewer viewer, ParticleEmitterData data, MSTSWagonV
6669
// Particles expand over time, this is just the initial volume, useful for calculating initial velocity
6770
ParticleVolumeM3 = 4.0f / 3.0f * MathHelper.Pi * ((EmitterData.NozzleDiameterM * EmitterData.NozzleDiameterM * EmitterData.NozzleDiameterM) / 8.0f);
6871
Emitter = new ParticleEmitterPrimitive(this, data, car, worldPosition);
72+
73+
if (!String.IsNullOrEmpty(EmitterData.Graphic))
74+
TexturePath = EmitterData.Graphic;
6975
#if DEBUG_EMITTER_INPUT
7076
EmitterID = ++EmitterIDIndex;
7177
InputCycle = Viewer.Random.Next(InputCycleLimit);
7278
#endif
73-
}
79+
}
7480

75-
public void Initialize(string textureName)
81+
public void Initialize(string defaultTextureName)
7682
{
77-
Material = (ParticleEmitterMaterial)Viewer.MaterialManager.Load("ParticleEmitter", textureName);
83+
bool customTexture = false;
84+
85+
if (!String.IsNullOrEmpty(TexturePath))
86+
customTexture = true;
87+
else
88+
TexturePath = defaultTextureName;
89+
90+
// Texture location preference is eng/wag folder -> MSTS GLOBAL\TEXTURES folder -> OR CONTENT folder
91+
if (File.Exists(Path.Combine(Path.GetDirectoryName(Emitter.CarViewer.Car.WagFilePath), TexturePath)))
92+
TexturePath = Path.Combine(Path.GetDirectoryName(Emitter.CarViewer.Car.WagFilePath), TexturePath);
93+
else if (File.Exists(Path.Combine(Viewer.Simulator.BasePath + @"\GLOBAL\TEXTURES\", TexturePath)))
94+
TexturePath = Path.Combine(Viewer.Simulator.BasePath + @"\GLOBAL\TEXTURES\", TexturePath);
95+
else if (customTexture && File.Exists(Path.Combine(Viewer.ContentPath, TexturePath)))
96+
TexturePath = Path.Combine(Viewer.ContentPath, TexturePath);
97+
else // Fall back to default texture in CONTENT folder
98+
{
99+
TexturePath = Path.Combine(Viewer.ContentPath, defaultTextureName);
100+
101+
if (customTexture)
102+
Trace.TraceWarning("Could not find particle graphic {0} at {1}", TexturePath, Path.Combine(Path.GetDirectoryName(Emitter.CarViewer.Car.WagFilePath), TexturePath));
103+
}
104+
105+
Material = (ParticleEmitterMaterial)Viewer.MaterialManager.Load("ParticleEmitter", TexturePath);
78106
}
79107

80108
/// <summary>
@@ -335,6 +363,8 @@ struct ParticleVertex
335363
internal float ParticleDuration;
336364
internal Color ParticleColor;
337365

366+
internal int SpriteCount;
367+
338368
internal float CompressionFactor = 1.0f;
339369

340370
internal WorldPosition WorldPosition;
@@ -373,6 +403,8 @@ public ParticleEmitterPrimitive(ParticleEmitterViewer particleViewer, ParticleEm
373403
ParticleDuration = 3;
374404
ParticleColor = Color.White;
375405

406+
SpriteCount = EmitterData.AtlasWidth * EmitterData.AtlasHeight;
407+
376408
CarViewer = car;
377409
WorldPosition = worldPosition;
378410

@@ -548,6 +580,7 @@ public void Update(float currentTime, ElapsedTime elapsedTime)
548580
rotY.Normalize();
549581

550582
float initialSpeed = XNAInitialVelocity.Length();
583+
Vector3 carVelocity = new Vector3(CarViewer.Velocity[0], CarViewer.Velocity[1], -CarViewer.Velocity[2]);
551584

552585
float time = currentTime - elapsedTime.ClockSeconds;
553586

@@ -559,12 +592,11 @@ public void Update(float currentTime, ElapsedTime elapsedTime)
559592

560593
int nextFreeParticle = (FirstFreeParticle + 1) % EmitterData.MaxParticles;
561594
int vertex = FirstFreeParticle * VerticesPerParticle;
562-
int texture = Viewer.Random.Next(16); // Randomizes emissions.
595+
int texture = Viewer.Random.Next(SpriteCount); // Randomizes particle texture to any texture on the sheet.
563596
// Alpha value of color is just a random number, not used (maybe allow for alpha changes?)
564597
Color color_Random = new Color(ParticleColor.R, ParticleColor.G, ParticleColor.B, (int)((float)Viewer.Random.NextDouble() * 255f));
565598

566599
Vector3 position = EmitterData.PositionM;
567-
Vector3 carVelocity = new Vector3(CarViewer.Velocity[0], CarViewer.Velocity[1], -CarViewer.Velocity[2]);
568600

569601
Vector3 initialVelocity = XNAInitialVelocity;
570602
Vector3 finalVelocity = XNAFinalVelocity;
@@ -764,6 +796,7 @@ public override void Render(GraphicsDevice graphicsDevice, IEnumerable<RenderIte
764796
var emitter = (ParticleEmitterPrimitive)item.RenderPrimitive;
765797
shader.EmitSize = emitter.EmitSize;
766798
shader.Texture = Texture;
799+
shader.TextureAtlasSizeXY = new Vector2(emitter.EmitterData.AtlasWidth, emitter.EmitterData.AtlasHeight);
767800
shader.SetMatrix(Matrix.Identity, ref XNAViewMatrix, ref XNAProjectionMatrix);
768801
ShaderPasses.Current.Apply();
769802
item.RenderPrimitive.Draw(graphicsDevice);

Source/RunActivity/Viewer3D/RollingStock/MSTSDieselLocomotiveViewer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public MSTSDieselLocomotiveViewer(Viewer viewer, MSTSDieselLocomotive car)
4141
// Now all the particle drawers have been setup, assign them textures based
4242
// on what emitters we know about.
4343

44-
string dieselTexture = viewer.Simulator.BasePath + @"\GLOBAL\TEXTURES\dieselsmoke.ace";
44+
string dieselTexture = "dieselsmoke.ace";
4545

4646

4747
// Diesel Exhaust

Source/RunActivity/Viewer3D/RollingStock/MSTSSteamLocomotiveViewer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public MSTSSteamLocomotiveViewer(Viewer viewer, MSTSSteamLocomotive car)
8080
{
8181
// Now all the particle drawers have been setup, assign them textures based
8282
// on what emitters we know about.
83-
string steamTexture = viewer.Simulator.BasePath + @"\GLOBAL\TEXTURES\smokemain.ace";
83+
string steamTexture = "smokemain.ace";
8484

8585
foreach (var emitter in ParticleDrawers)
8686
{

Source/RunActivity/Viewer3D/RollingStock/MSTSWagonViewer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ public MSTSWagonViewer(Viewer viewer, MSTSWagon car)
115115
: base(viewer, car)
116116
{
117117

118-
string steamTexture = viewer.Simulator.BasePath + @"\GLOBAL\TEXTURES\smokemain.ace";
119-
string dieselTexture = viewer.Simulator.BasePath + @"\GLOBAL\TEXTURES\dieselsmoke.ace";
118+
string steamTexture = "smokemain.ace";
119+
string dieselTexture = "dieselsmoke.ace";
120120

121121
// Particle Drawers called in Wagon so that wagons can also have steam effects.
122122
ParticleDrawers = (

Source/RunActivity/Viewer3D/Shaders.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,7 @@ public class ParticleEmitterShader : Shader
468468
EffectParameter wvp;
469469
EffectParameter invView;
470470
EffectParameter texture;
471+
EffectParameter textureAtlasSize;
471472
EffectParameter lightVector;
472473
EffectParameter fog;
473474

@@ -486,6 +487,11 @@ public Texture2D Texture
486487
set { texture.SetValue(value); }
487488
}
488489

490+
public Vector2 TextureAtlasSizeXY
491+
{
492+
set { textureAtlasSize.SetValue(value); }
493+
}
494+
489495
public float EmitSize
490496
{
491497
set { emitSize.SetValue(value); }
@@ -505,6 +511,7 @@ public ParticleEmitterShader(GraphicsDevice graphicsDevice)
505511
invView = Parameters["invView"];
506512
tileXY = Parameters["cameraTileXY"];
507513
texture = Parameters["particle_Tex"];
514+
textureAtlasSize = Parameters["texAtlasSize"];
508515
lightVector = Parameters["LightVector"];
509516
fog = Parameters["Fog"];
510517
}

0 commit comments

Comments
 (0)