HOWTO: Render FPS weapon
In this tutorial, you will learn how to create a custom PostFx script and use it to render player weapon in a first-person shooter game. It features:
- separate weapon rendering pass
- no clipping with world geometry
- customizable visuals for a weapon (eg. can disable SSAO/SSR)
- customizable render projection (weapon can be rendered with own FOV/Frustum)
- customizable lighting (eg. weapon can be affected only by sunlight).
1. Create script
Create a new script and add code that performs the effect rendering. Use PostProcessEffect class that inherits from Script and can be used as postfx on camera and view.
using FlaxEngine;
/// <summary>
/// PostFx script for custom player weapon rendering over world geometry. Attached to the player camera actor.
/// </summary>
public class WeaponRenderer : PostProcessEffect
{
private Camera _camera;
private SceneRenderTask _renderingTask;
private GPUTexture _outputTexture;
private GPUPipelineState _compositeOutputPipeline;
/// <summary>
/// Layer (or layers) that are used by weapon. Used to hide weapon from default scene rendering in the Game viewport.
/// </summary>
public LayersMask WeaponLayer = new LayersMask(0);
/// <summary>
/// Layer (or layers) that are used by additional objects used by weapon rendering (eg. lights, postfx volumes).
/// </summary>
public LayersMask AdditionalLayers = new LayersMask(0);
/// <summary>
/// Rendering features to use for weapon rendering (limited to main rendering - PostFx are configured in the main view drawing).
/// </summary>
public ViewFlags RenderFlags = ViewFlags.DirectionalLights |
ViewFlags.SkyLights |
ViewFlags.SpotLights |
ViewFlags.PointLights |
ViewFlags.SpecularLight |
ViewFlags.Shadows |
ViewFlags.ContactShadows |
ViewFlags.Fog |
ViewFlags.Reflections |
ViewFlags.GI;
/// <summary>
/// If checked then weapon will be rendered with custom projection (customizable FOV and near/far planes).
/// </summary>
[EditorDisplay("Projection")]
public bool CustomProjection = false;
/// <summary>
/// Custom Field Of View angle for the weapon rendering.
/// </summary>
[Range(30, 120), EditorDisplay("Projection"), VisibleIf(nameof(CustomProjection))]
public float WeaponFov = 60.0f;
/// <summary>
/// Custom Near Plane distance for the weapon rendering.
/// </summary>
[Limit(0.0001f), EditorDisplay("Projection"), VisibleIf(nameof(CustomProjection))]
public float WeaponNearPlane = 0.1f;
/// <summary>
/// Custom Far Plane distance for the weapon rendering.
/// </summary>
[Limit(10.0f), EditorDisplay("Projection"), VisibleIf(nameof(CustomProjection))]
public float WeaponFarPlane = 10000.0f;
public WeaponRenderer()
{
// Render weapons after scene is rendered but before any PostFx
Location = PostProcessEffectLocation.AfterForwardPass;
UseSingleTarget = true;
}
public override void OnEnable()
{
_camera = Actor.As<Camera>();
if (_camera == null)
{
Debug.LogError("Attach WeaponRenderer to the player camera actor.", this);
return;
}
// Disable weapons drawing in camera's view
_camera.RenderLayersMask &= ~WeaponLayer;
// Create new rendering task to draw
_outputTexture = GPUDevice.Instance.CreateTexture("WeaponTexture");
_renderingTask = new SceneRenderTask
{
IsCustomRendering = true, // Don't use automatic rendering but manually schedule rendering
Output = _outputTexture,
};
_renderingTask.Buffers.UseAlpha = true;
// Create PSO that will draw weapon over the scene (rendered weapon is alpha-masked)
var psoDesc = GPUPipelineState.Description.DefaultFullscreenTriangle;
psoDesc.PS = GPUDevice.Instance.QuadShader.GetPS("PS_CopyLinear");
psoDesc.BlendMode = BlendingMode.AlphaBlend;
psoDesc.BlendMode.SrcBlend = BlendingMode.Blend.One;
psoDesc.BlendMode.DestBlend = BlendingMode.Blend.InvSrcAlpha;
psoDesc.BlendMode.BlendOp = BlendingMode.Operation.Add;
psoDesc.BlendMode.SrcBlendAlpha = BlendingMode.Blend.One;
psoDesc.BlendMode.DestBlendAlpha = BlendingMode.Blend.Zero;
psoDesc.BlendMode.BlendOp = BlendingMode.Operation.Add;
_compositeOutputPipeline = new GPUPipelineState();
_compositeOutputPipeline.Init(ref psoDesc);
}
public override void OnDisable()
{
// Cleanup
Destroy(ref _compositeOutputPipeline);
Destroy(ref _outputTexture);
Destroy(ref _renderingTask);
_camera = null;
}
public override void Render(GPUContext context, ref RenderContext renderContext, GPUTexture input, GPUTexture output)
{
if (!_renderingTask)
return;
var camera = _camera ?? Actor.As<Camera>();
if (!camera)
return;
Profiler.BeginEventGPU("Weapon");
var width = input.Width;
var height = input.Height;
// Initialize weapon rendering
var view = renderContext.View;
view.Mode = ViewMode.NoPostFx; // Just render weapon with lighting, postfx are applied once for a whole game view
view.RenderLayersMask = WeaponLayer | AdditionalLayers; // Render both weapon and lighting
view.Flags = RenderFlags; // Select visual features to use during rendering
if (CustomProjection)
{
// Customize projection matrix
view.Near = WeaponNearPlane;
view.Far = WeaponFarPlane;
float aspect = (float)width / (float)height;
float fov = WeaponFov * Mathf.DegreesToRadians;
Matrix.PerspectiveFov(fov, aspect, view.Near, view.Far, out view.Projection);
view.NonJitteredProjection = view.Projection;
view.UpdateCachedData();
}
_renderingTask.View = view;
// Setup rendering resolution
if (!_outputTexture.IsAllocated)
{
var outputDesc = GPUTextureDescription.New2D(width, height, _renderingTask.Buffers.OutputFormat);
_outputTexture.Init(ref outputDesc);
}
_renderingTask.Resize(width, height);
// Reuse main game viewport GI/GlobalSDF/etc when rendering weapon
_renderingTask.Buffers.LinkedCustomBuffers = renderContext.Buffers;
// Render nested scene with weapon-only
Renderer.Render(_renderingTask);
// Composite weapon over the scene view
context.ResetRenderTarget();
context.SetViewport(width, height);
context.SetRenderTarget(input.View());
context.BindSR(0, _outputTexture.View());
var pipeline = _compositeOutputPipeline;
context.SetState(pipeline);
context.DrawFullscreenTriangle();
Profiler.EndEventGPU();
}
}
2. Setup layers
Open Layers and Tags settings and add separate layers for Weapons
and Lights
. Then set those layers to proper actors so the game knows which object is player's weapon, and which are lights on a scene.
3. Setup scene
Now, add created script to the Camera actor, link WeaponLayer property of the script to the layer with player weapon objects. Link AdditionalLayers property to global PostFx volume, Environment probes and Lights that should be used when rendering weapons.
You can also adjust rendering settings via RenderFlags property or override the projection matrix used for weapon rendering.
4. Test it out!
Finally start the game and see the result.