godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.12k stars 69 forks source link

Add ParticleRenderMaterial (built-in material for particles that works for the most common particle usecases) #5046

Open QbieShay opened 2 years ago

QbieShay commented 2 years ago

Describe the project you are working on

Various VFX and Godot

Describe the problem or limitation you are having in your project

Godot doesn't offer a good out-of-the-box material for particles. Specifically, while SpatialMaterial has been improved tremendously, it's too big for particles and making defaults that work with particles would compromise the UX for everyone else.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

Note: the following shader works best for simple stylized particles. For realistic particles that use normal maps, motion vectors and emission maps, we will need a more complicated shader. But it would be probably best to just have a motion vector node in Visual Shaders, since the format of these textures isn't 100% standard.

Add a new material ParticleRendererMaterial:

shader_type spatial;
render_mode unshaded;

//Note: convert to instance uniforms where possible
//perhaps make a VFX node that bakes all textures and use a texture array?

uniform sampler2D albedo_texture: hint_default_white; // called albedo for compatibility
// the material should assign source_color to albedo_texture if gradient map is off
uniform ivec2 animation_frames = ivec2(1);
uniform bool particles_anim_loop = false;
uniform bool use_color_map;
uniform sampler2D color_map: source_color;
uniform bool animation_loop = false;
uniform int billboard = 0;
uniform bool proximity_fade = false;
uniform float proximity_fade_distance = 1.0;
uniform bool erosion;
uniform float erosion_hardness = 0.0;
uniform sampler2D erosion_texture: hint_default_white;
// possibly add motion vectors

varying vec2 flipbook_uv;

float overlay(float base, float blend){
    float limit = step(0.5, base);
    return mix(2.0 * base * blend, 1.0 - 2.0 * (1.0 - base) * (1.0 - blend), limit);
}

void vertex(){
    if(billboard == 0){
        // particle billboard
        // keep scale
        mat4 mat_world = mat4(normalize(INV_VIEW_MATRIX[0]) * length(MODEL_MATRIX[0]), normalize(INV_VIEW_MATRIX[1]) * length(MODEL_MATRIX[0]),normalize(INV_VIEW_MATRIX[2]) * length(MODEL_MATRIX[2]), MODEL_MATRIX[3]);
        mat_world = mat_world * mat4(vec4(cos(INSTANCE_CUSTOM.x), -sin(INSTANCE_CUSTOM.x), 0.0, 0.0), vec4(sin(INSTANCE_CUSTOM.x), cos(INSTANCE_CUSTOM.x), 0.0, 0.0), vec4(0.0, 0.0, 1.0, 0.0), vec4(0.0, 0.0, 0.0, 1.0));
        MODELVIEW_MATRIX = VIEW_MATRIX * mat_world;
    }
    if (billboard == 1){
        // y billboard
        // keep scale
        MODELVIEW_MATRIX = VIEW_MATRIX * mat4(vec4(normalize(cross(vec3(0.0, 1.0, 0.0), INV_VIEW_MATRIX[2].xyz)), 0.0), vec4(0.0, 1.0, 0.0, 0.0), vec4(normalize(cross(INV_VIEW_MATRIX[0].xyz, vec3(0.0, 1.0, 0.0))), 0.0), MODEL_MATRIX[3]);
        MODELVIEW_MATRIX = MODELVIEW_MATRIX * mat4(vec4(length(MODEL_MATRIX[0].xyz), 0.0, 0.0, 0.0),vec4(0.0, length(MODEL_MATRIX[1].xyz), 0.0, 0.0), vec4(0.0, 0.0, length(MODEL_MATRIX[2].xyz), 0.0), vec4(0.0, 0.0, 0.0, 1.0));
    }
    float h_frames = float(animation_frames.x);
    float v_frames = float(animation_frames.y);
    float particle_total_frames = float(animation_frames.x * animation_frames.y);
    float particle_frame = floor(INSTANCE_CUSTOM.z * float(particle_total_frames));
    if (!particles_anim_loop) {
        particle_frame = clamp(particle_frame, 0.0, particle_total_frames - 1.0);
    } else {
        particle_frame = mod(particle_frame, particle_total_frames);
    }
    flipbook_uv = UV;
    flipbook_uv /= vec2(h_frames, v_frames);
    flipbook_uv += vec2(mod(particle_frame, h_frames) / h_frames, floor((particle_frame + 0.5) / h_frames) / v_frames);
}

void fragment(){
    vec4 color = texture(albedo_texture, flipbook_uv);
    // map colors from the supplied gradient
    if (use_color_map){
        vec4 gradient_color = texture(color_map, vec2(color.r));
        color.rgb = gradient_color.rgb;
        color.a = color.a * gradient_color.a;
    }
    ALBEDO = color.rgb * COLOR.rgb;
    if (erosion){
        float erosion_value = texture(erosion_texture, UV).r;
        ALPHA = overlay(COLOR.a, erosion_value) * color.a;
        ALPHA = smoothstep(0.0, 1.0 - erosion_hardness, ALPHA);
    } else {
        ALPHA = COLOR.a * color.a;
    }
    ALPHA = clamp(0.0, 1.0, ALPHA);
    if(proximity_fade){
        float depth_tex = textureLod(DEPTH_TEXTURE,SCREEN_UV,0.0).r;
        vec4 world_pos = INV_PROJECTION_MATRIX * vec4(SCREEN_UV*2.0-1.0,depth_tex,1.0);
        world_pos.xyz/=world_pos.w;
        ALPHA*=clamp(1.0-smoothstep(world_pos.z+proximity_fade_distance,world_pos.z,VERTEX.z),0.0,1.0);
    }
}

Download shader + scene: https://gfycat.com/scalyofficialbighorn DefaultParticleMaterial.zip

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

Download and try the following shader:

If this enhancement will not be used often, can it be worked around with a few lines of script?

Yes it can, but it's about good built-in materials. Plus, it enables

GPUParticles should be auto-assigned a process material and an appropriate mesh+material when created (link to proposal(s))

from https://github.com/godotengine/godot-proposals/issues/5044

Is there a reason why this should be core and not an add-on in the asset library?

Improve editor and Godot UX for VFX Artists, out of the box.

clayjohn commented 2 years ago

I think instead of having a custom shader for ParticlesRenderMaterial, we should instead have ParticlesRenderMaterial inherit from BaseMaterial so it has all the same options as StandardMaterial, but can have all the Particles-specific features enabled by default. That way we can maintain consistency between the Materials. We could still add particle-specific code to BaseMaterial and then disable them by default when using StandardMaterial, or have particle-specific code in ParticlesRenderMaterial.

The situation I would like to avoid is the awkwardness we have now with Sprite3D and AnimatedSprite3D where users add an override material because they want a single effect from StandardMaterial, but then they suddenly lose some of the helpful fields that are built into Sprite3D.

QbieShay commented 2 years ago

Hey @clayjohn ,

Most VFX use custom shaders. I don't think it makes sense to add the ramp and erosion to the standard material, considering that they are very specific to particles.

I understand what you're saying, but particle materials are radically different in usage from normal materials, unless you go for realism, and in that case our spatial material already works well

QbieShay commented 2 years ago

Additionally, i feel like Spatial Material got a lot of features precisely because we decided to not specialize it so far. It's used for static meshes, skinned meshes, emissive material, transparent materials..

Overall, it just does a lot out of the box, of which many things are exclusive with each other

clayjohn commented 2 years ago

We could add features to ParticlesRenderMaterial not present in StandardMaterial. But so far Godot's design has been to let you combine any rendering feature with particles, they aren't limited to typical billboarded quads, accordingly, things like fade, emission, normal maps, etc. all just work out of the box.

I don't feel good about adding features to a ParticleRenderMaterial and not exposing the other features as you run into the problem where someone wants something seemingly basic like normal maps or emission, but they are not available. Then we are stuck either duplicating code or forcing the user to make a custom shader.

QbieShay commented 2 years ago

Using SpatialMaterial is still possible for particles. This material would just be the one assigned by default to a particle system

clayjohn commented 2 years ago

Using SpatialMaterial is still possible for particles. This material would just be the one assigned by default to a particle system

The problem is if someone needs a feature only available in SpatialMaterial and another feature only available in ParticlesRenderMaterial.

QbieShay commented 2 years ago

At that point they'd make a shader. Ideally, all basic functionalities (normal map, erosion, etc.) will be supplied each in a VS node so recreating these behaviours is not hard.

Again, this isn't meant to cover all particle uses. It's meant to be a first step for people that use particles, making it easy for them to obtain a good looking particle system out of the box, without having to tweak all the parameters in a spatial material, which for particles is full of noise.

In most cases, VFX artists will make custom shaders anyway.

Of note, this shader is the product of professional experience in stylized games. With a shader like this one, I can cover most of the particles I need.

I had started with an ubershader in the past (see here https://qbieshay.itch.io/godot-vfx-kit). I still consider the shader proposed here an easy to learn and tweak first step for people (and artists unfamiliar with shaders) starting out. Using kits like this one https://www.artstation.com/marketplace/p/YDnN0/handpainted-flipbooks-01-assets-for-vfx-artists?utm_source=artstation&utm_medium=referral&utm_campaign=homepage&utm_term=marketplace is not possible with spatial material.