1
\$\begingroup\$

I would like to know, how to make that effect, I have seen in the game named Yomawari. The little girl has a lamp and its cone revals the dark spirits around her. But only if they are in the light. I think it's like the additive blending, but I dont get it to work. I use monogame and c#.

Here is a sample: https://youtu.be/kPLRXP7ovB0?t=1241

All ideas are wellcome (sourcecode preferred ;))

Greetings Kai

My current test code: (DL: https://github.com/EnemyArea/yomawari-lights)

    protected override void Draw(GameTime gameTime)
    {
        var blend = new BlendState
        {
            ColorBlendFunction = BlendFunction.Add,
            ColorSourceBlend = Blend.DestinationColor,
            ColorDestinationBlend = Blend.Zero
        };

        var lightBlendState = new BlendState
        {
            ColorSourceBlend = Blend.Zero,
            ColorDestinationBlend = Blend.SourceColor
        };

        var mousePosition = Mouse.GetState().Position;

        // Draw lights
        this.GraphicsDevice.SetRenderTarget(this.blackpixel);
        this.GraphicsDevice.Clear(Color.DarkGray);
        this.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive);
        this.spriteBatch.Draw(this.lichtRund, new Vector2(mousePosition.X, mousePosition.Y), null, Color.White, 0f, new Vector2(this.lichtRund.Width * 0.5f, this.lichtRund.Height * 0.5f), 1f, SpriteEffects.None, 0);
        this.spriteBatch.End();
        this.GraphicsDevice.SetRenderTarget(null);

        // Draw hidden
        this.GraphicsDevice.SetRenderTarget(this.renderTarget);
        this.GraphicsDevice.Clear(Color.White);
        this.spriteBatch.Begin(SpriteSortMode.Deferred, blend);
        this.spriteBatch.Draw(this.testeye, new Vector2(this.GraphicsDevice.Viewport.Width / 2f, this.GraphicsDevice.Viewport.Height / 2f), null, Color.White, 0f, new Vector2(this.testeye.Width * 0.5f, this.testeye.Height * 0.5f), 1f, SpriteEffects.None, 0);
        this.spriteBatch.Draw(this.blackpixel, Vector2.Zero, Color.White);
        this.spriteBatch.End();
        this.GraphicsDevice.SetRenderTarget(null);

        // Combine all
        this.GraphicsDevice.Clear(Color.Black);

        this.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend);
        this.spriteBatch.Draw(this.background, Vector2.Zero, Color.White);
        this.spriteBatch.End();

        this.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend);
        this.spriteBatch.Draw(this.renderTarget, Vector2.Zero, Color.White);
        this.spriteBatch.End();

        this.spriteBatch.Begin(SpriteSortMode.Deferred, lightBlendState);
        this.spriteBatch.Draw(this.blackpixel, Vector2.Zero, Color.White);
        this.spriteBatch.End();

        base.Draw(gameTime);
    }

it results into that: Wrong result but is should look like enter image description here

another sample transperant

\$\endgroup\$
  • \$\begingroup\$ Is it the deferred lighting effect? I wrote an article about it: felsirworld.net/deferred-2d-lights (if not can you point to the timestamp in the video what specific effect you’re looking for?) \$\endgroup\$ – Felsir Nov 24 '18 at 20:43
  • \$\begingroup\$ is 2 seconds later, when she lights up the dark spirit. \$\endgroup\$ – EnemyArea Nov 24 '18 at 20:47
  • \$\begingroup\$ Likely still the deferred lighting effect. The way the lightcone works is that a separate texture controls what is visible and what is invisible. So the ‘spirit’ is also made visible via that texture. \$\endgroup\$ – Felsir Nov 24 '18 at 20:55
  • \$\begingroup\$ but isn't it the same as if you use the additive blending? Having a floor, a black texture over it and a "lightsource" ? I just want to make the pixels from a texture visible, proportional to the "lightsource"-texture \$\endgroup\$ – EnemyArea Nov 24 '18 at 21:07
  • \$\begingroup\$ Yes. If you have a black texture with a white circle on it, you could use it as a texture mask. In the game, they already use the deferred effect for the flashlight so it is a bonus of an effect they already have in place. \$\endgroup\$ – Felsir Nov 24 '18 at 21:17
2
\$\begingroup\$

Here is a solution using a shader. Basically the shader uses the lightmap to mask pixels. The lightmap is black and white texture and the R (red) component is used to determine if the mask should be applied or not. This leaves the G and B components usable for a different effect.

First the shader:

    #if OPENGL
    #define SV_POSITION POSITION
    #define VS_SHADERMODEL vs_3_0
    #define PS_SHADERMODEL ps_3_0
#else
    #define VS_SHADERMODEL vs_4_0_level_9_1
    #define PS_SHADERMODEL ps_4_0_level_9_1
#endif

Texture2D SpriteTexture;

sampler2D SpriteTextureSampler = sampler_state
{
    Texture = <SpriteTexture>;
};

Texture2D MaskTexture;

sampler2D MaskTextureSampler = sampler_state
{
    Texture = <MaskTexture>;
};



struct VertexShaderOutput
{
    float4 Position : SV_POSITION;
    float4 Color : COLOR0;
    float2 TextureCoordinates : TEXCOORD0;
};

float4 MainPS(VertexShaderOutput input) : COLOR
{
    float4 col = tex2D(SpriteTextureSampler,input.TextureCoordinates) *input.Color; //sample the source image
    float mask = tex2D(MaskTextureSampler, input.TextureCoordinates).r; //sample the R component of the mask image.

    if(mask>0) // if the mask is >0 apply it.
        return float4(col.r,col.g,col.b,col.a*mask); // keep all colors, and use the alpha of the source.

    return float4(0, 0, 0, 0); //otherwise return a transparent (masked) pixel- without color information.
}

technique SpriteDrawing
{
    pass P0
    {
        PixelShader = compile PS_SHADERMODEL MainPS();
    }
};

This is the Draw function:

    /// <summary>
    ///     This is called when the game should draw itself.
    /// </summary>
    /// <param name="gameTime">Provides a snapshot of timing values.</param>
    protected override void Draw(GameTime gameTime)
    {

        // Draw lights
        this.GraphicsDevice.SetRenderTarget(this.LightmapLayer);
        this.GraphicsDevice.Clear(Color.Black);
        this.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive);
        this.spriteBatch.Draw(this.Lightmap, new Vector2(this.GraphicsDevice.Viewport.Width / 2f, this.GraphicsDevice.Viewport.Height / 2f), null, Color.White, 0f, new Vector2(this.Lightmap.Width * 0.5f, this.Lightmap.Height * 0.5f), 1f, SpriteEffects.None, 0);
        this.spriteBatch.End();
        this.GraphicsDevice.SetRenderTarget(null);

        //I created a simple rendertarget 'GhostLayer':
        this.GraphicsDevice.SetRenderTarget(this.GhostLayer);
        this.GraphicsDevice.Clear(Color.Transparent);
        this.spriteBatch.Begin();
        this.spriteBatch.Draw(this.Ghost, new Vector2(this.GraphicsDevice.Viewport.Width / 2f, this.GraphicsDevice.Viewport.Height / 2f), null, Color.White, 0f, new Vector2(this.Ghost.Width * 0.5f, this.Ghost.Height * 0.5f), 1f, SpriteEffects.None, 0);
        this.spriteBatch.End();


        //show the result on the screen:
        this.GraphicsDevice.SetRenderTarget(null);
        this.GraphicsDevice.Clear(Color.Purple);

        //Draw the game scene:
        this.spriteBatch.Begin();
        this.spriteBatch.Draw(this.Background, Vector2.Zero, Color.White);
        this.spriteBatch.End();

        //Draw the ghosts using a mask effect:
        this.MaskEffect.Parameters["MaskTexture"].SetValue(LightmapLayer);

        this.spriteBatch.Begin(SpriteSortMode.Immediate, null, null, null, null, MaskEffect);
        this.spriteBatch.Draw(this.GhostLayer, Vector2.Zero, Color.White);
        this.spriteBatch.End();

        //Draw the lights:
        this.spriteBatch.Begin();
        this.spriteBatch.Draw(this.LightmapLayer, Vector2.Zero, Color.White*0.25f);
        this.spriteBatch.End();


        base.Draw(gameTime);
    }

Based on the code in your github, it produces this result:

TestEyeGame with masked lightbeam

The final light is now simply drawn in, this could be made more subtle with another shader effect.

\$\endgroup\$
  • \$\begingroup\$ Thank u, thats a very nice solution too. I came to a little bit diffrent solution today too. Many thanks for your help. \$\endgroup\$ – EnemyArea Nov 26 '18 at 18:43
2
\$\begingroup\$

Answer with the use of a shader elsewhere. I leave this answer because the technique may be of interest to someone.

You can create your own blendstate like this:

        MultiplicativeBlend = new BlendState();
        //deal with transparency
        MultiplicativeBlend.AlphaBlendFunction = BlendFunction.ReverseSubtract;
        MultiplicativeBlend.AlphaSourceBlend = Blend.SourceAlpha;
        MultiplicativeBlend.AlphaDestinationBlend = Blend.Zero;
        //deal with color
        MultiplicativeBlend.ColorBlendFunction = BlendFunction.Add;
        MultiplicativeBlend.ColorSourceBlend = Blend.DestinationColor;
        MultiplicativeBlend.ColorDestinationBlend = Blend.Zero;

This creates a multiplicative blend, while keeping the transparency of the source.

With these textures:

The lightmap:

Lightmap

The ghost (this texture represent all the 'ghosts' in the scene only visible in the lightbeam:

Ghost picture

The game scene, as drawn normally without the ghosts:

Backdrop

The image can be drawn with the mask (using the multiply blendstate). In the code below I set up the components and two custom blendstates. One is the "multiply" blendstate to mask the ghost. The other is the "Light" blendstate to render the lightbeam later:

    protected override void LoadContent()
    {
        // Create a new SpriteBatch, which can be used to draw textures.
        spriteBatch = new SpriteBatch(GraphicsDevice);

        Ghost = Content.Load<Texture2D>("ghost");
        Lightmap = Content.Load<Texture2D>("lightmask");
        Background = Content.Load<Texture2D>("background");

        // TODO: use this.Content to load your game content here
        MultiplicativeBlend = new BlendState();
        //deal with transparency
        MultiplicativeBlend.AlphaBlendFunction = BlendFunction.ReverseSubtract;
        MultiplicativeBlend.AlphaSourceBlend = Blend.SourceAlpha;
        MultiplicativeBlend.AlphaDestinationBlend = Blend.Zero;
        //deal with color
        MultiplicativeBlend.ColorBlendFunction = BlendFunction.Add;
        MultiplicativeBlend.ColorSourceBlend = Blend.DestinationColor;
        MultiplicativeBlend.ColorDestinationBlend = Blend.Zero;

        //Another blendstate to deal with the lightmap later:
        LightBlend = new BlendState();
        LightBlend.ColorBlendFunction = BlendFunction.Subtract;
        LightBlend.ColorSourceBlend = Blend.DestinationColor;
        LightBlend.ColorDestinationBlend = Blend.Zero;

        GhostLayer = new RenderTarget2D(GraphicsDevice, 250, 250);
    }

Now putting it in use like this:

    protected override void Draw(GameTime gameTime)
    {

        //I created a simple 250x250 pixel rendertarget 'GhostLayer':
        GraphicsDevice.SetRenderTarget(GhostLayer);
        GraphicsDevice.Clear(Color.Transparent);

        //first draw the ghost in the normal way
        spriteBatch.Begin();
        spriteBatch.Draw(Ghost, Vector2.Zero, Color.White); 
        spriteBatch.End();

        //now draw the lightmap to mask so only the 'ghost', 
        //this uses the blendstate created above.
        spriteBatch.Begin(SpriteSortMode.Immediate, MultiplicativeBlend);
        spriteBatch.Draw(Lightmap, Vector2.Zero, Color.White); 
        spriteBatch.End();


        //show the result on the screen:
        GraphicsDevice.SetRenderTarget(null);
        GraphicsDevice.Clear(Color.Black);

        spriteBatch.Begin();
        //Draw the game scene:
        spriteBatch.Draw(Background, Vector2.Zero, Color.White);
        //Draw the (masked) ghosts:
        spriteBatch.Draw(GhostLayer, Vector2.Zero, Color.White);

        // uncomment the next 'Draw' line and comment out the 'lightblend' 
        // section to see the alternate result.
        //spriteBatch.Draw(Lightmap, Vector2.Zero, Color.White*0.5f);  

        spriteBatch.End();

        //Lightblend section:
        //Draw the lightbeam using the lightBlend (though a shader might be better for this part).
        spriteBatch.Begin(SpriteSortMode.Immediate,LightBlend);
        spriteBatch.Draw(Lightmap, Vector2.Zero, Color.White);
        spriteBatch.End();
        //End lightblend section.

        base.Draw(gameTime);
    }

Now the final result is the ghost is clipped according to the lightmap, with the lightmap showing.

The result of the above code

I think, this produces the effect you're looking for.

This is the effect where the lightmap is drawn without the "lightblend" effect, to demonstrate the red circle is indeed clipped:

Lightmap without lightblend

Though I recommend taking a look at the sample code https://github.com/Felsir/LightingEngine as it shows a C#, Monogame example how the light effect is achieved by using a shader. This produces a more subtle effect with better control over ambient light and light flow.

It produces this effect:

Light effect using shaders

\$\endgroup\$
  • \$\begingroup\$ thank you, I try it :) looks promising. Please wait. \$\endgroup\$ – EnemyArea Nov 24 '18 at 21:37
  • \$\begingroup\$ Ok, something is wrong.. I tried to use it within my lighting engine but got only a black screen. I edit my post. \$\endgroup\$ – EnemyArea Nov 24 '18 at 22:09
  • \$\begingroup\$ Updated the answer with sample code and a screenshot of the produced effect. \$\endgroup\$ – Felsir Nov 25 '18 at 18:29
  • \$\begingroup\$ awesome! It works :D But one last thing, why is it yellow? its a red circle. \$\endgroup\$ – EnemyArea Nov 25 '18 at 19:50
  • \$\begingroup\$ That's because the light effect is blended over it in the final draw step- mixing it to that hue. The final light beam pass can be done better than in this example. I think the best part it to demonstrate the mask for the red circle using a different texture- the lightbeam can be blended in another pass like in your sample code. \$\endgroup\$ – Felsir Nov 25 '18 at 20:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.