#Simple example(s) of render to texture, then render that texture on top of everything else? (2D)

57 messages · Page 1 of 1 (latest)

tacit prairie
#

Basically I want to put a solid black layer on top of the normal render and punch transparent holes in it so you can only see specific things. (Think torches from Link to the Past).

I'm pretty noob when it comes to rendering in general, so the less complex the example(s), the better. Also knowing the actual names of the concepts I'm trying to do would be great too lol

rough elm
#

I suppose you could just add the mask on front and use shaders to punch holes into it

#

I don't think you actually need to render the game to a texture first

#

Step 1: Get a custom material with a fragment shader to show on top of the game
Step 2: Pass the torch positions to the shader with uniforms
Step 3: Make everything not next to a torch black within the shader

#

This is quite advanced so please ask if something is unclear. Lots of moving parts and things to just take on blind faith for now

#

Essentially, the material is defined as a struct that implements Material2d, a trait used for materials in 2d renders. After you have that, add it to the game with .add_plugins(Material2dPlugin::<YourCustomStruct>::default()); or some other way of instantiating an object of YourCustomStruct

#

The fields of the struct can get annotated to pass them to the shaders

#

I suggest you use wgsl as a shader language. The example places the shader in assets/shader/custom_material.wgsl but I suggest a more relevant name like torch_mask.wgsl

#

What's missing from the example is the actual fragment shader function. You could do something like:


@fragment
fn fragment(
    mesh: MeshVertexOutput,
) -> @location(0) vec4<f32> {
    return material.color;
}

To achieve step 1

#

Step 1 is probably the most work, get back to me after you're done with all that

#

You also don't need any fields besides the color in the struct for now and you don't need the color_texture or color_sampler variables in the shader. They may even cause problems if you include them

tacit prairie
#

Oh cool, discord didn't notify me of a response. I will take a look at all of this when I get a chance, thanks!

tacit prairie
#

@rough elm one thing I noticed, the custom_material.wgsl example uses #import bevy_pbr::mesh_vertex_output MeshVertexOutput but I'm not using PBR, I have the feature disabled currently. Is that something I'm going to need to turn back on or is there an alternative?

#

Hmm... I found a different shader that doesn't have that that looks more 2d centric. Going to mess around a little with this and see what happens

#

Oh, I got it to stop erroring and render something. I'm kind of surprised lol. Step 1 complete?

rough elm
#

Yay

#

Aight so for step 2 add a Vec<Vec3> to the material and tag it as a uniform

#

And in the shader add a uniform to read it in

#

In the material creation, add a single Vec3::Zero for testing

#

In the shader code loop through the incoming points and set the color to something identifiable if the pixel is close to a point

#

You may also have to pass the length of the point list in as a separate field to correctly set the array size in wgsl

#

You may run into issues with position spaces. Coordinates in shaders tend to be normalized from -1 to 1 and world space coordinates are not. We can fix this later which is why I told you to put the testing torch at origin

#

Send a picture when dome so I can validate. You should end up with a circle in the middle of the screen

rough elm
tacit prairie
#

what I have so far is:

#import bevy_sprite::mesh2d_bindings        mesh
#import bevy_sprite::mesh2d_functions       mesh2d_position_local_to_clip

struct LightingMaterial {
    color: vec4<f32>,
};

struct Vertex {
    @location(0) position: vec3<f32>,
};

struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
};

@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
    var out: VertexOutput;
    out.clip_position = mesh2d_position_local_to_clip(mesh.model, vec4<f32>(vertex.position, 1.0));
    return out;
}

@group(1) @binding(0)
var<uniform> material: LightingMaterial;

@fragment
fn fragment() -> @location(0) vec4<f32> {
    return material.color;
}
#

when I try to add a Vec<Vec3> to the material, the AsBindGroup derive complains that the trait bound "Vec<bevy::prelude::Vec3>: ShaderSize" is not satisfied
I'm not sure why, but I tried again and it's not giving that error now, will try getting the logic in the shader working now

tacit prairie
#

hmm.. now I'm getting panicked at 'runtime-sized array can't be used in uniform buffers'

#

I've got to do something else for a bit, but I might come back and try a static sized array with a count

tacit prairie
#

it doesn't seem to like adding a fixed array to the material either. I did get the local->world working in the vertex shader and have it passing though to the fragment shader, so that's cool

tacit prairie
#

I've got two hard coded light 2d-position/radius/intensity defined in the shader and that's working =) now I just need to figure out step 2 (yes, the oval shape is intentional lol)

rough elm
#

Could you try to instead of a dynamically sized Vec<Vec3>, do a statically sized [Vec3;20] and then on the other side something similar

#

Could also do something like pass in an image with a single marker pixel at each torch location and go based on that

tacit prairie
#

I got this working via storage instead of uniform, though I'm going to try the static array in a uniform again because I think I messed up the syntax the first time and I should be able to fit this in a uniform I think

rough elm
#

Welp in any case, step 3 is to just set the "not near a torch" -color(black?) to each pixel, loop over the array and if the pixel is near one, set it transparent. That way the pixels near the torches are transparent and the unlit areas are dark

tacit prairie
#

it's working! thanks for all your help!

rough elm
#

Can you show it? I'm curious

tacit prairie
#

code or screenshot?

rough elm
#

Both would be nice. Sometimes people search these threads for help instead of asking

#

I'm also learning here

tacit prairie
#

I'll post the code once I clean it up a bit so there's not just tons of commented out junk in the way lol

rough elm
#

The overlap is a bit weird

#

I think you only ever want to decrease the alpha value

tacit prairie
#

it should be doing alpha * alpha, which should result in a lower alpha. not really sure why it looks strange on the borders

rough elm
#

Hard to say without seeing the code. How do you calculate the new alpha value?

#

It should probably do something like alpha = min(alpha, newAlpha)

tacit prairie
#

in theory I should be multiplying two values in the [0, 1] range, but I'll try that and see what happens

rough elm
#

It somehow looks darker in the overlap, so I think something weird is going on here

tacit prairie
#

here's the shader:

#import bevy_sprite::mesh2d_bindings        mesh
#import bevy_sprite::mesh2d_functions       mesh2d_position_local_to_clip
#import bevy_sprite::mesh2d_functions       mesh2d_position_local_to_world

struct PointLight {
    position: vec2<f32>,
    radius: f32,
    intensity: f32,
}

struct LightingData {
    global_color: vec4<f32>,
    light_count: i32,
    _padding: vec3<f32>,
    lights: array<PointLight, 64>,
}

struct Vertex {
    @location(0) position: vec3<f32>,
};

struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
    @location(0) world_pos: vec2<f32>,
};

@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
    var out: VertexOutput;
    var pos4 = vec4<f32>(vertex.position, 1.0);

    out.clip_position = mesh2d_position_local_to_clip(mesh.model, pos4);

    let world = mesh2d_position_local_to_world(mesh.model, pos4);
    out.world_pos = vec2<f32>(world.x, world.y);

    return out;
}

@group(1) @binding(0)
var<uniform> light_data: LightingData;

struct FragmentInput {
    @location(0) world_pos: vec2<f32>,
};

@fragment
fn fragment(input: FragmentInput) -> @location(0) vec4<f32> {
    var out_color = light_data.global_color;

    for (var i = 0; i < light_data.light_count; i++) {
        let light = light_data.lights[i];
        
        let offset = input.world_pos - light.position;
        // Squish the Y radius for isometric aesthetic reasons
        let radius_squished = vec2<f32>(light.radius, light.radius * 3.0 / 4.0);
        let ratio = offset / radius_squished;
        let ratio_sq = ratio * ratio;

        let dist_scalar = 1.0 - clamp(sqrt(ratio_sq.x + ratio_sq.y), 0.0, 1.0);
        let final_intensity = dist_scalar * light.intensity;

        out_color.a *= 1.0 - final_intensity;
    }
    
    return out_color;
}
#

and here's the rust:

use bevy::{
    prelude::*,
    sprite::{Material2d, Material2dPlugin, MaterialMesh2dBundle},
    render::render_resource::{AsBindGroup, ShaderRef, ShaderType},
    reflect::{TypeUuid, TypePath}
};
use crate::prelude::*;

#[derive(AsBindGroup, TypeUuid, TypePath, Debug, Clone)]
#[uuid = "dc31daf2-d31e-43ae-b31a-95e5377f7f10"]
pub struct LightingMaterial {
    #[uniform(0)]
    light_data: LightingData,
}

#[derive(Clone, ShaderType, Debug)]
pub struct LightingData {
    pub global_color: Color,
    pub light_count: i32,
    pub _padding: Vec3,
    pub lights: [PointLight; 64],
}
impl Default for LightingData {
    fn default() -> Self {
        Self {
            global_color: Color::Rgba { red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0 },
            light_count: 0,
            _padding: Vec3::ZERO,
            lights: [PointLight::default(); 64],
        }
    }
}

#[derive(Default, Clone, Copy, ShaderType, Debug)]
pub struct PointLight {
    pub position: Vec2,
    pub radius: f32,
    pub intensity: f32,
}

impl Material2d for LightingMaterial {
    fn fragment_shader() -> ShaderRef {
        "shaders/lighting.wgsl".into()
    }
    fn vertex_shader() -> ShaderRef {
        "shaders/lighting.wgsl".into()
    }
}
#
pub struct Plugin;
impl bevy::app::Plugin for Plugin {
    fn build (&self, app: &mut App) {
        app.add_plugins(Material2dPlugin::<LightingMaterial>::default());
        app.add_systems(Startup, setup);
    }
}

fn setup (
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<LightingMaterial>>,
) {
    let mut light_data = LightingData::default();

    // Set up some lights for testing
    light_data.light_count = 3;
    light_data.lights[0] = PointLight { position: Vec2::ZERO, radius: 5.0, intensity: 0.75 };
    light_data.lights[1] = PointLight { position: Vec2::X * 7.0, radius: 3.0, intensity: 0.5 };
    light_data.lights[2] = PointLight { position: Vec2::NEG_ONE * 6.0, radius: 3.0, intensity: 0.5 };

    let screen_units = Vec2::new(1920.0, 1080.0) / s_bevy_transform::PIXELS_PER_UNIT;

    // Just giving it a high Z so it draws on top of in game stuff
    let material_position = Vec3::new(0.0, 0.0, 100.0);

    commands.spawn(MaterialMesh2dBundle {
        mesh: meshes.add(Mesh::from(shape::Quad::default())).into(),
        transform: Transform {
            translation: material_position,
            scale: screen_units.extend(1.0),
            ..default()
        },
        material: materials.add(LightingMaterial {
            light_data: light_data,
        }),
        ..default()
    });
}
#

I tried adding a min on the alpha call and it looks the same as before

#

I wonder if it's some sort of weird optical illusion type thing? that'd be weird lol

#

if I put one of the small lights inside the big one it looks right

#

scooted it over and made it dimmer and it still looks good

rough elm
#

TBH, no clue

#

Why the _padding?