Skip to content

Commit

Permalink
Customizable image sample mapping (djeedai#245)
Browse files Browse the repository at this point in the history
Add a new `ImageSampleMapping` enum to customize the way a sample of
the image of a `ParticleTextureModifier` is mapped to and modulated with
the particle's base color.

The previous behavior was always using the Red channel of the texture as
an opacity mask, modulating the sample's Alpha component only. This
legacy behavior can be restored with the `ModulateOpacityFromR` value.

The new default is `Modulate`, which corresponds to a full
component-wise multiply of all components of the image sample by the
particle's base color.
  • Loading branch information
djeedai authored Nov 3, 2023
1 parent 8eea4c1 commit 9407aa6
Show file tree
Hide file tree
Showing 9 changed files with 86 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added implementations of `ToWgslString` for the missing vector types (`UVec2/3/4`, `IVec2/3/4`, `BVec2/3/4`).
- Added new `CastExpr` expression to cast an operand expression to another `ValueType`. This adds a new variant `Expr::Cast` too.
- Added new `BinaryOperator::Remainder` to calculate the remainder (`%` operator) of two expressions.
- Added the `ImageSampleMapping` enum to determine how samples of the image of a `ParticleTextureModifier` are mapped to and modulated with the particle's base color. The new default behavior is `ImageSampleMapping::Modulate`, corresponding to a full modulate of all RGBA components. To restore the previous behavior, and use the Red channel of the texture as an opacity mask, set `ParticleTextureModifier::sample_mapping` to `ImageSampleMapping::ModulateOpacityFromR`.

### Changed

Expand Down
1 change: 1 addition & 0 deletions examples/billboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ fn setup(
.init(init_lifetime)
.render(ParticleTextureModifier {
texture: texture_handle,
sample_mapping: ImageSampleMapping::ModulateOpacityFromR,
})
.render(OrientModifier {
mode: OrientMode::ParallelCameraDepthPlane,
Expand Down
1 change: 1 addition & 0 deletions examples/circle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ fn setup(
.init(init_lifetime)
.render(ParticleTextureModifier {
texture: texture_handle.clone(),
sample_mapping: ImageSampleMapping::ModulateOpacityFromR,
})
.render(ColorOverLifetimeModifier { gradient })
.render(SizeOverLifetimeModifier {
Expand Down
1 change: 1 addition & 0 deletions examples/gradient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ fn setup(
.init(init_lifetime)
.render(ParticleTextureModifier {
texture: texture_handle.clone(),
sample_mapping: ImageSampleMapping::ModulateOpacityFromR,
})
.render(ColorOverLifetimeModifier { gradient }),
);
Expand Down
8 changes: 7 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,7 @@ impl EffectShaderSource {
alpha_cutoff_code,
particle_texture,
layout_flags,
image_sample_mapping_code,
) = {
let mut render_context = RenderContext::new(&property_layout, &particle_layout);
for m in asset.render_modifiers() {
Expand Down Expand Up @@ -990,6 +991,7 @@ impl EffectShaderSource {
alpha_cutoff_code,
render_context.particle_texture,
layout_flags,
render_context.image_sample_mapping_code,
)
};

Expand Down Expand Up @@ -1067,7 +1069,11 @@ impl EffectShaderSource {
"{{SIMULATION_SPACE_TRANSFORM_PARTICLE}}",
&render_sim_space_transform_code,
)
.replace("{{ALPHA_CUTOFF}}", &alpha_cutoff_code);
.replace("{{ALPHA_CUTOFF}}", &alpha_cutoff_code)
.replace(
"{{PARTICLE_TEXTURE_SAMPLE_MAPPING}}",
&image_sample_mapping_code,
);
trace!("Configured render shader:\n{}", render_shader_source);

Ok(EffectShaderSource {
Expand Down
26 changes: 16 additions & 10 deletions src/modifier/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ impl SetAttributeModifier {
pub fn new(attribute: Attribute, value: ExprHandle) -> Self {
Self { attribute, value }
}

fn eval(
&self,
module: &mut Module,
context: &mut dyn EvalContext,
) -> Result<String, ExprError> {
assert!(module.get(self.value).is_some());
let attr = module.attr(self.attribute);
let attr = context.eval(module, attr)?;
let expr = context.eval(module, self.value)?;
Ok(format!("{} = {};\n", attr, expr))
}
}

#[typetag::serde]
Expand Down Expand Up @@ -115,11 +127,8 @@ impl Modifier for SetAttributeModifier {
#[typetag::serde]
impl InitModifier for SetAttributeModifier {
fn apply_init(&self, module: &mut Module, context: &mut InitContext) -> Result<(), ExprError> {
assert!(module.get(self.value).is_some());
let attr = module.attr(self.attribute);
let attr = context.eval(module, attr)?;
let expr = context.eval(module, self.value)?;
context.init_code += &format!("{} = {};\n", attr, expr);
let code = self.eval(module, context)?;
context.init_code += &code;
Ok(())
}
}
Expand All @@ -131,11 +140,8 @@ impl UpdateModifier for SetAttributeModifier {
module: &mut Module,
context: &mut UpdateContext,
) -> Result<(), ExprError> {
assert!(module.get(self.value).is_some());
let attr = module.attr(self.attribute);
let attr = context.eval(module, attr)?;
let expr = context.eval(module, self.value)?;
context.update_code += &format!("{} = {};\n", attr, expr);
let code = self.eval(module, context)?;
context.update_code += &code;
Ok(())
}
}
4 changes: 4 additions & 0 deletions src/modifier/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,9 @@ pub struct RenderContext<'a> {
pub render_extra: String,
/// Texture modulating the particle color.
pub particle_texture: Option<Handle<Image>>,
/// WGSL code describing how to modulate the base color of the particle with
/// the image texture sample, if any.
pub image_sample_mapping_code: String,
/// Color gradients.
pub gradients: HashMap<u64, Gradient<Vec4>>,
/// Size gradients.
Expand All @@ -490,6 +493,7 @@ impl<'a> RenderContext<'a> {
fragment_code: String::new(),
render_extra: String::new(),
particle_texture: None,
image_sample_mapping_code: String::new(),
gradients: HashMap::new(),
size_gradients: HashMap::new(),
screen_space_size: false,
Expand Down
51 changes: 51 additions & 0 deletions src/modifier/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,52 @@ use crate::{
Module, RenderContext, RenderModifier, ShaderCode, ToWgslString,
};

/// Mapping of the sample read from a texture image to the base particle color.
///
/// This defines the way the texture image of [`ParticleTextureModifier`] blends
/// with the base particle color to define the final render color of the
/// particle.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
pub enum ImageSampleMapping {
/// Modulate the particle's base color with the full RGBA sample of the
/// texture image.
///
/// ```wgsl
/// color = baseColor * texColor;
/// ```
#[default]
Modulate,

/// Modulate the particle's base color with the RGB sample of the texture
/// image, leaving the alpha component unmodified.
///
/// ```wgsl
/// color.rgb = baseColor.rgb * texColor.rgb;
/// ```
ModulateRGB,

/// Modulate the alpha component (opacity) of the particle's base color with
/// the red component of the sample of the texture image.
///
/// ```wgsl
/// color.a = baseColor.a * texColor.r;
/// ```
ModulateOpacityFromR,
}

impl ToWgslString for ImageSampleMapping {
fn to_wgsl_string(&self) -> String {
match *self {
ImageSampleMapping::Modulate => "color = color * texColor;",
ImageSampleMapping::ModulateRGB => {
"color = vec4<f32>(color.rgb * texColor.rgb, color.a);"
}
ImageSampleMapping::ModulateOpacityFromR => "color.a = color.a * texColor.r;",
}
.to_string()
}
}

/// A modifier modulating each particle's color by sampling a texture.
///
/// # Attributes
Expand All @@ -22,6 +68,9 @@ pub struct ParticleTextureModifier {
// representation... NOTE - Need to keep a strong handle here, nothing else will keep that
// texture loaded currently.
pub texture: Handle<Image>,

/// The mapping of the texture image samples to the base particle color.
pub sample_mapping: ImageSampleMapping,
}

impl_mod_render!(ParticleTextureModifier, &[]); // TODO - should require some UV maybe?
Expand All @@ -30,6 +79,7 @@ impl_mod_render!(ParticleTextureModifier, &[]); // TODO - should require some UV
impl RenderModifier for ParticleTextureModifier {
fn apply_render(&self, _module: &mut Module, context: &mut RenderContext) {
context.set_particle_texture(self.texture.clone());
context.image_sample_mapping_code = self.sample_mapping.to_wgsl_string();
}
}

Expand Down Expand Up @@ -312,6 +362,7 @@ mod tests {
let texture = Handle::<Image>::default();
let modifier = ParticleTextureModifier {
texture: texture.clone(),
..default()
};

let mut module = Module::default();
Expand Down
9 changes: 4 additions & 5 deletions src/render/vfx_render.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,11 @@ fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {

{{FRAGMENT_MODIFIERS}}

#ifdef PARTICLE_TEXTURE
var color = textureSample(particle_texture, particle_sampler, in.uv);
color = vec4<f32>(1.0, 1.0, 1.0, color.r); // FIXME - grayscale modulate
color = in.color * color;
#else
var color = in.color;

#ifdef PARTICLE_TEXTURE
var texColor = textureSample(particle_texture, particle_sampler, in.uv);
{{PARTICLE_TEXTURE_SAMPLE_MAPPING}}
#endif

#ifdef USE_ALPHA_MASK
Expand Down

0 comments on commit 9407aa6

Please sign in to comment.