Summary
On macOS, Local Fog Volumes have no visible effect at all in a -game run or a Movie Render Queue render. The volume is registered with the scene: the debug overlay text reports its start distance. But nothing is drawn, not even the tile debug colours of r.LocalFogVolume.TileDebug 2, and that holds with volumetric fog disabled, as the documentation requires for the debug view. Exponential Height Fog, volumetric fog and Volume-domain materials render correctly in the same scene.
Unreal Engine 5.8.4 (Launcher build, CL 58902052, ++UE5+Release-5.8)
- macOS 15.6.1 (24G90)
- MacBook Pro Mac15,11, Apple M3 Max (30-core GPU), 36 GB unified memory, Metal 3
- RHI: Metal, shader platform METAL_SM6; driver internal 329.2
- Project settings default for Local Fog Volumes (
r.SupportLocalFogVolumes1). Deferred renderer, Lumen GI and reflections, virtual shadow maps.
What Type of Bug are you experiencing?
Level Design & Art Tools
Steps to Reproduce
Empty level containing: a floor, three spot lights, an Exponential Height Fog (density 0.05, volumetric fog on), a post-process volume with manual exposure, and a camera at (-1000, 0, 260) looking down +X.
2. Add a Local Fog Volume at (300, -500, 200), scale 0.6, Radial Fog Extinction 2 to 5, Height Fog Extinction 0, Fog Emissive (4, 0.6, 0.3) or up to (40, 4, 1.5), albedo near white.
3. Render it with Movie Render Queue (-game -LevelSequence=... -MoviePipelineConfig=...), or run the level with -game and take HighResShot.
4. Repeat with r.VolumetricFog 0 and r.LocalFogVolume.TileDebug 2.
voltest.py (attached) builds the whole test level, sequence and render configs headless (UnrealEditor-Cmd … -run=pythonscript -script=voltest.py`). The same level also holds a Volume-domain material mesh, which renders correctly.
Expected Result
A visible emissive or extinguishing sphere of fog at the volume’s location.
- With
TileDebug 2and volumetric fog off, coloured debug tiles over the volume’s screen area.
Observed Result
No visible change at the volume’s location in any configuration: the image is identical to a scene without the volume.
- With
TileDebug 2, only the canvas text appears: “LOCAL FOG VOLUME DEBUG VIEW / Global Start Distance = 1000.0 / Instances Start Distance = 1092.8”. No tile colours are drawn anywhere, with volumetric fog on or off, in-gameor MRQ. - These settings changed nothing (MRQ console variables, also set with
-ExecCmds):r.LocalFogVolume.RenderIntoVolumetricFog 0r.LocalFogVolume.RenderDuringHeightFogPass 1r.LocalFogVolume.UseHZB 0withr.LocalFogVolume.TileCullingUseAsync 0r.LocalFogVolume.GlobalStartDistance 10r.LocalFogVolume.MaxDensityIntoVolumetricFog 1.0
- These volume changes didn’t help: an arbitrarily rotated volume (to rule out the packed rotation reading back as denormals) and very high emissive.
- Adding a Sky Atmosphere and a visible Volumetric Cloud didn’t help either. A forum user on an M3 Max reported that as a workaround (thread linked below).
Notes from reading the 5.8 source
ShouldRenderLocalFogVolumereturns true: the debug overlay text is drawn byRenderLocalFogVolumeVisualization, which requires it.- In debug mode 2,
LocalFogVolumeTiledPSclips only when the contribution equals identity. No tiles appear, so either the tiled culling (LocalFogVolumeTiledCulling.usf) produces zero tiles or no indirect draws, or every pixel evaluates to identity inGetLFVInstanceContribution. - The per-instance data goes through a
Buffer<float4>SRV (PF_A32B32G32R32F) with integer bit patterns (half pairs, R11G11B10, RGBA8) and is read back withasuint. If Metal flushes denormal floats on typed buffer loads, that data could be corrupted. A rotated volume didn’t change the result, though, so this is unconfirmed. - Possibly related reports, none with an Epic answer or bug number:
- Local Fog Volumes show in the viewport but not in final renders (Sep 2025): Local Fog visible in viewport but not rendered
- Local fog shows as static noise on Mac in 5.4.4, while the PC build is fine: Local Fog not working on Mac version of Project
- Local fog volume “simply gone” on an M2 Max in 5.4.0: UE 5.4.0 is much slower than UE 5.3.2 on Mac Metal
- Black blocks around local fog on an M3 Max in 5.3.2: Local Height Fog always comes with black pixels in foreground
Affects Versions
5.8
Platform(s)
Mac
Upload an image
Additional Notes
A small test level for fog volumes in Movie Render Queue, apart from any film’s level: three ways of putting fog in
one place, side by side under three spots, in a thin height fog. Left: Local Fog Volume. Middle: a mesh with a Volume
domain material (voxelized into the volumetric fog). Right: a translucent box raymarching its own fog.
ue_run.sh scripts/voltest.py, then voltest.sh base|debug|novox|clamp|hfog|nohzb|near|novf|debugnovf
import unreal
LEVEL = ‘/Game/voltest’
OUT = ‘/Users/bogdan/av-studio/renders/voltest’
les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
eas = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
eal = unreal.EditorAssetLibrary
mel = unreal.MaterialEditingLibrary
at = unreal.AssetToolsHelpers.get_asset_tools()
CUBE = unreal.load_asset(‘/Engine/BasicShapes/Cube’)
X, Z = 300.0, 200.0
YS = {‘lfv’: -500.0, ‘vol’: 0.0, ‘ray’: 500.0}
SIZE = 3.0 # the volumes’ scale: 300 cm across
def fresh(path):
if eal.does_asset_exist(path):
eal.delete_asset(path)
def material(name):
path = ‘/Game/mat/’ + name
fresh(path)
return at.create_asset(name, ‘/Game/mat’, unreal.Material, unreal.MaterialFactoryNew())
def const3(m, v, x, y):
e = mel.create_material_expression(m, unreal.MaterialExpressionConstant3Vector, x, y)
e.set_editor_property(‘constant’, unreal.LinearColor(*v, 1.0))
return e
def const1(m, v, x, y):
e = mel.create_material_expression(m, unreal.MaterialExpressionConstant, x, y)
e.set_editor_property(‘r’, v)
return e
PATCH = ‘’’
float3 d = (WP - Obj) / R; // -1..1 across the volume’s mesh
float edge = saturate(1.0 - length(d));
float3 p = WP * 0.004 + float3(Time * 0.05, -Time * 0.03, Time * 0.02);
float n = 0.0, amp = 0.5;
for (int i = 0; i < 4; i++) {
float3 f = frac(p), q = floor(p);
f = f * f * (3.0 - 2.0 * f);
float h000 = frac(sin(dot(q, float3(127.1, 311.7, 74.7))) * 43758.5);
float h100 = frac(sin(dot(q + float3(1, 0, 0), float3(127.1, 311.7, 74.7))) * 43758.5);
float h010 = frac(sin(dot(q + float3(0, 1, 0), float3(127.1, 311.7, 74.7))) * 43758.5);
float h110 = frac(sin(dot(q + float3(1, 1, 0), float3(127.1, 311.7, 74.7))) * 43758.5);
float h001 = frac(sin(dot(q + float3(0, 0, 1), float3(127.1, 311.7, 74.7))) * 43758.5);
float h101 = frac(sin(dot(q + float3(1, 0, 1), float3(127.1, 311.7, 74.7))) * 43758.5);
float h011 = frac(sin(dot(q + float3(0, 1, 1), float3(127.1, 311.7, 74.7))) * 43758.5);
float h111 = frac(sin(dot(q + float3(1, 1, 1), float3(127.1, 311.7, 74.7))) * 43758.5);
n += amp * lerp(lerp(lerp(h000, h100, f.x), lerp(h010, h110, f.x), f.y), lerp(lerp(h001, h101, f.x), lerp(h011, h111, f.x), f.y), f.z);
p = p * 2.03 + 1.7;
amp *= 0.5;
}
return Ext * edge * edge * saturate((n - 0.3) * 2.5);
‘’’
def volume_domain():
“”“Fog in one place: a mesh whose Volume material is voxelized into the volumetric fog, so the scene’s lights
scatter in it (with their shadows). Density: soft-edged inside the mesh, drifting noise; no glow of its own.”“”
m = material(‘M_voltest_volume’)
m.set_editor_property(‘material_domain’, unreal.MaterialDomain.MD_VOLUME)
m.set_editor_property(‘blend_mode’, unreal.BlendMode.BLEND_ADDITIVE)
mel.connect_material_property(const3(m, (1.0, 0.91, 0.81), -300, 0), ‘’, unreal.MaterialProperty.MP_BASE_COLOR)
c = mel.create_material_expression(m, unreal.MaterialExpressionCustom, -500, 300)
c.set_editor_property(‘code’, PATCH)
c.set_editor_property(‘output_type’, unreal.CustomMaterialOutputType.CMOT_FLOAT1)
inputs =
for n in (‘WP’, ‘Obj’, ‘R’, ‘Time’, ‘Ext’):
ci = unreal.CustomInput()
ci.set_editor_property(‘input_name’, n)
inputs.append(ci)
c.set_editor_property(‘inputs’, inputs)
src = {‘WP’: mel.create_material_expression(m, unreal.MaterialExpressionWorldPosition, -900, 200),
‘Obj’: mel.create_material_expression(m, unreal.MaterialExpressionObjectPositionWS, -900, 300),
‘R’: const1(m, 50.0 * SIZE, -900, 400),
‘Time’: mel.create_material_expression(m, unreal.MaterialExpressionTime, -900, 500),
‘Ext’: const1(m, 6.0, -900, 600)}
for n, e in src.items():
mel.connect_material_expressions(e, ‘’, c, n)
# (a Volume material takes its extinction on the Subsurface Color pin, not Opacity)
mel.connect_material_property(c, ‘’, unreal.MaterialProperty.MP_SUBSURFACE_COLOR)
mel.recompile_material(m)
eal.save_loaded_asset(m)
return m
RAY = ‘’’
float3 rd = normalize(WP - Cam);
float3 ro = Cam - C;
float3 inv = 1.0 / rd;
float3 a = (-H - ro) * inv, b = (H - ro) * inv;
float3 lo = min(a, b), hi = max(a, b);
float t0 = max(max(max(lo.x, lo.y), lo.z), 0.0);
float t1 = min(min(hi.x, hi.y), hi.z);
float toSurface = Depth / max(dot(rd, ResolvedView.ViewForward), 0.001);
t1 = min(t1, toSurface);
if (t1 <= t0) return float4(0, 4, 0, 0.5); // (debug: green where the ray misses)
const int N = 24;
float dt = (t1 - t0) / N;
float T = 1.0;
float3 glow = 0;
for (int i = 0; i < N; i++) {
float3 p = ro + rd * (t0 + dt * (i + 0.5));
float fall = saturate(1.0 - length(p) / H);
float w = sin(p.x * 0.021 + 1.0) * sin(p.y * 0.017 + 2.0) * sin(p.z * 0.023)
+ 0.5 * sin(p.x * 0.053 + p.z * 0.02) * sin(p.y * 0.047 + 4.0) * sin(p.z * 0.061 + 1.0);
float d = Dens * fall * fall * saturate(0.6 + w);
float aa = 1.0 - exp(-d * dt);
glow += T * aa * Col * saturate(0.35 + 0.65 * (p.z + H) / (2.0 * H)); // lit from above
T *= 1.0 - aa;
}
return float4(glow, 1.0 - T);
‘’’
def raymarch():
m = material(‘M_voltest_ray’)
m.set_editor_property(‘blend_mode’, unreal.BlendMode.BLEND_TRANSLUCENT)
m.set_editor_property(‘shading_model’, unreal.MaterialShadingModel.MSM_UNLIT)
m.set_editor_property(‘disable_depth_test’, True)
c = mel.create_material_expression(m, unreal.MaterialExpressionCustom, -500, 0)
c.set_editor_property(‘code’, RAY)
c.set_editor_property(‘output_type’, unreal.CustomMaterialOutputType.CMOT_FLOAT4)
names = [‘WP’, ‘Cam’, ‘Depth’, ‘C’, ‘H’, ‘Dens’, ‘Col’]
inputs =
for n in names:
ci = unreal.CustomInput()
ci.set_editor_property(‘input_name’, n)
inputs.append(ci)
c.set_editor_property(‘inputs’, inputs)
src = {‘WP’: mel.create_material_expression(m, unreal.MaterialExpressionWorldPosition, -900, 0),
‘Cam’: mel.create_material_expression(m, unreal.MaterialExpressionCameraPositionWS, -900, 100),
‘Depth’: mel.create_material_expression(m, unreal.MaterialExpressionSceneDepth, -900, 200),
‘C’: const3(m, (X, YS[‘ray’], Z), -900, 300),
‘H’: const1(m, 50.0 * SIZE, -900, 400),
‘Dens’: const1(m, 0.02, -900, 500),
‘Col’: const3(m, (4.0, 3.2, 2.4), -900, 600)}
for n, e in src.items():
mel.connect_material_expressions(e, ‘’, c, n)
rgb = mel.create_material_expression(m, unreal.MaterialExpressionComponentMask, -200, 0)
for k, v in zip(‘rgba’, (True, True, True, False)):
rgb.set_editor_property(k, v)
al = mel.create_material_expression(m, unreal.MaterialExpressionComponentMask, -200, 150)
for k, v in zip(‘rgba’, (False, False, False, True)):
al.set_editor_property(k, v)
mel.connect_material_expressions(c, ‘’, rgb, ‘’)
mel.connect_material_expressions(c, ‘’, al, ‘’)
mel.connect_material_property(rgb, ‘’, unreal.MaterialProperty.MP_EMISSIVE_COLOR)
mel.connect_material_property(al, ‘’, unreal.MaterialProperty.MP_OPACITY)
mel.recompile_material(m)
eal.save_loaded_asset(m)
return m
def box(loc, scale, mat=None):
a = eas.spawn_actor_from_class(unreal.StaticMeshActor, unreal.Vector(*loc))
a.static_mesh_component.set_static_mesh(CUBE)
if mat not in (None, M_GREY):
a.static_mesh_component.set_cast_shadow(False)
a.set_actor_scale3d(unreal.Vector(*scale))
if mat:
a.static_mesh_component.set_material(0, mat)
return a
M_VOL, M_RAY = volume_domain(), raymarch()
M_GREY = material(‘M_voltest_grey’)
mel.connect_material_property(const3(M_GREY, (0.15, 0.15, 0.15), -300, 0), ‘’, unreal.MaterialProperty.MP_BASE_COLOR)
mel.recompile_material(M_GREY)
eal.save_loaded_asset(M_GREY)
if eal.does_asset_exist(LEVEL): # (a loaded level can’t be replaced: clear it instead)
les.load_level(LEVEL)
for old in eas.get_all_level_actors():
eas.destroy_actor(old)
else:
les.new_level(LEVEL)
box((0, 0, -5), (40, 40, 0.1), M_GREY)
box((1000, 0, 400), (0.1, 40, 10), M_GREY)
w = eas.spawn_actor_from_class(unreal.RectLight, unreal.Vector(400, 0, 300), unreal.Rotator(0, 0, 0))
w.rect_light_component.set_editor_property(‘intensity’, 40.0)
w.rect_light_component.set_editor_property(‘source_width’, 3000.0)
for key, y in YS.items():
s = eas.spawn_actor_from_class(unreal.SpotLight, unreal.Vector(X, y, 700), unreal.Rotator(0, -90, 0))
lc = s.spot_light_component
lc.set_editor_property(‘intensity_units’, unreal.LightUnits.CANDELAS)
lc.set_editor_property(‘intensity’, 8000.0)
lc.set_editor_property(‘attenuation_radius’, 1500.0)
lc.set_editor_property(‘outer_cone_angle’, 22.0)
lc.set_editor_property(‘inner_cone_angle’, 12.0)
lc.set_editor_property(‘volumetric_scattering_intensity’, 4.0)
lc.set_editor_property(‘mobility’, unreal.ComponentMobility.MOVABLE)
fog = eas.spawn_actor_from_class(unreal.ExponentialHeightFog, unreal.Vector(0, 0, 0))
fc = fog.component
fc.set_editor_property(‘fog_density’, 0.05)
fc.set_editor_property(‘fog_height_falloff’, 0.001)
fc.set_editor_property(‘fog_inscattering_luminance’, unreal.LinearColor(0, 0, 0, 1))
fc.set_editor_property(‘enable_volumetric_fog’, True)
fc.set_editor_property(‘volumetric_fog_distance’, 6000.0)
pp = eas.spawn_actor_from_class(unreal.PostProcessVolume, unreal.Vector(0, 0, 0))
pp.set_editor_property(‘unbound’, True)
st = pp.settings
st.set_editor_property(‘override_auto_exposure_method’, True)
st.set_editor_property(‘auto_exposure_method’, unreal.AutoExposureMethod.AEM_MANUAL)
st.set_editor_property(‘override_auto_exposure_apply_physical_camera_exposure’, True)
st.set_editor_property(‘auto_exposure_apply_physical_camera_exposure’, False)
st.set_editor_property(‘override_auto_exposure_bias’, True)
st.set_editor_property(‘auto_exposure_bias’, -6.0)
pp.set_editor_property(‘settings’, st)
1 Local Fog Volume
(rotated a little on every axis: an unrotated volume’s packed rotation reads back as denormal floats, which Apple
GPUs flush to zero, and the volume draws nothing)
lf = eas.spawn_actor_from_class(unreal.LocalFogVolume, unreal.Vector(X, YS[‘lfv’], Z), unreal.Rotator(roll=11.0, pitch=7.0, yaw=37.0))
lf.set_actor_scale3d(unreal.Vector(0.6, 0.6, 0.6)) # (its base size is 1000 cm across)
lfc = lf.get_component_by_class(unreal.LocalFogVolumeComponent)
lfc.set_editor_property(‘radial_fog_extinction’, 2.0)
lfc.set_editor_property(‘fog_albedo’, unreal.LinearColor(1.0, 0.9, 0.8, 1.0))
lfc.set_editor_property(‘height_fog_extinction’, 0.0)
lfc.set_editor_property(‘fog_emissive’, unreal.LinearColor(4.0, 0.6, 0.3, 1.0))
print(‘LFV mobility’, lfc.get_editor_property(‘mobility’), ‘visible’, lfc.is_visible())
2 a mesh with a Volume domain material
box((X, YS[‘vol’], Z), (SIZE, SIZE, SIZE), M_VOL)
3 a box raymarching its own fog
box((X, YS[‘ray’], Z), (SIZE, SIZE, SIZE), M_RAY)
cam = eas.spawn_actor_from_class(unreal.CineCameraActor, unreal.Vector(-1000, 0, 260), unreal.Rotator(0, -3, 0))
cam.get_cine_camera_component().set_editor_property(‘current_focal_length’, 18.0)
the same view for a plain game run (no Movie Render Queue): the default pawn spawns here
eas.spawn_actor_from_class(unreal.PlayerStart, unreal.Vector(-1000, 0, 200), unreal.Rotator(0, 0, 0))
les.save_current_level()
path = ‘/Game/seq_voltest’
fresh(path)
seq = at.create_asset(‘seq_voltest’, ‘/Game’, unreal.LevelSequence, unreal.LevelSequenceFactoryNew())
seq.set_display_rate(unreal.FrameRate(24, 1))
seq.set_playback_start(0)
seq.set_playback_end(3)
b = seq.add_possessable(cam)
cuts = seq.add_track(unreal.MovieSceneCameraCutTrack)
cs = cuts.add_section()
cs.set_range(0, 3)
bid = unreal.MovieSceneObjectBindingID()
bid.set_editor_property(‘guid’, b.get_id())
cs.set_camera_binding_id(bid)
eal.save_loaded_asset(seq)
def config(name, cvars):
p = ‘/Game/mrq/voltest_’ + name
fresh(p)
cfg = at.create_asset(‘voltest_’ + name, ‘/Game/mrq’, unreal.MoviePipelinePrimaryConfig,
unreal.MoviePipelinePrimaryConfigFactory())
out = cfg.find_or_add_setting_by_class(unreal.MoviePipelineOutputSetting)
out.output_directory = unreal.DirectoryPath(OUT)
out.file_name_format = name + ‘_{frame_number}’
out.output_resolution = unreal.IntPoint(1280, 720)
out.override_existing_output = True
cfg.find_or_add_setting_by_class(unreal.MoviePipelineDeferredPassBase)
cfg.find_or_add_setting_by_class(unreal.MoviePipelineImageSequenceOutput_JPG)
aa = cfg.find_or_add_setting_by_class(unreal.MoviePipelineAntiAliasingSetting)
aa.override_anti_aliasing = True
aa.anti_aliasing_method = unreal.AntiAliasingMethod.AAM_TSR
aa.engine_warm_up_count = 24
aa.render_warm_up_count = 24
aa.use_camera_cut_for_warm_up = False
cv = cfg.find_or_add_setting_by_class(unreal.MoviePipelineConsoleVariableSetting)
for k, v in cvars:
cv.add_or_update_console_variable(k, v)
eal.save_loaded_asset(cfg)
config(‘base’, ())
config(‘debug’, ((‘r.LocalFogVolume.TileDebug’, 2),))
config(‘novox’, ((‘r.LocalFogVolume.RenderIntoVolumetricFog’, 0),))
config(‘clamp’, ((‘r.LocalFogVolume.MaxDensityIntoVolumetricFog’, 1.0),))
config(‘hfog’, ((‘r.LocalFogVolume.RenderDuringHeightFogPass’, 1),))
config(‘nohzb’, ((‘r.LocalFogVolume.UseHZB’, 0), (‘r.LocalFogVolume.TileCullingUseAsync’, 0)))
config(‘near’, ((‘r.LocalFogVolume.GlobalStartDistance’, 10.0), (‘r.LocalFogVolume.RenderIntoVolumetricFog’, 0)))
config(‘novf’, ((‘r.VolumetricFog’, 0),))
config(‘debugnovf’, ((‘r.VolumetricFog’, 0), (‘r.LocalFogVolume.TileDebug’, 2)))
print(‘BUILT voltest’)



