Editor: Added ability to hide/unhide utility brushes and target lines.

Tweaked path tracing so lighting is more realistic.
This commit is contained in:
Justin Marshall
2026-05-11 09:35:14 -07:00
parent e7ff9997ce
commit 4da1776627
2 changed files with 273 additions and 43 deletions
+104 -42
View File
@@ -3374,6 +3374,35 @@ float3 LoadSceneSpecularAlbedo(uint2 pixel, float3 baseAlbedo)
return Doom3PseudoSpecularMask(baseAlbedo);
}
float ComputeDiffuseLightingTerm(float3 N, float3 L)
{
// Keep the original light volume/range attenuation exactly where it is, but
// make the surface response less gamey. The old 0.28-0.32 Half-Lambert wrap
// pushed too much light around silhouettes and into back-facing normal-map
// detail. This smaller squared wrap keeps Doom/idTech readability while
// giving a more Lambert-like, physically plausible rolloff.
float rawNoL = dot(normalize(N), normalize(L));
if (gEnableHalfLambert != 0u)
{
const float REALISTIC_WRAP = 0.12;
float wrapped = saturate((rawNoL + REALISTIC_WRAP) / (1.0 + REALISTIC_WRAP));
return wrapped * wrapped;
}
return saturate(rawNoL);
}
float EstimateSpecularRoughness(float3 specularAlbedo)
{
// No roughness map is available in this G-buffer path, so infer a stable
// perceptual roughness from the specular map strength. Brighter spec maps
// get tighter highlights; dark/missing maps stay broad and subdued.
float peak = SpecularPeak3(saturate(specularAlbedo));
return clamp(lerp(0.68, 0.34, peak), 0.28, 0.72);
}
float3 ComputeSpecular(
float3 N,
float3 V,
@@ -3387,47 +3416,66 @@ float3 ComputeSpecular(
if (gEnableSpecular == 0)
return 0.0;
if (atten <= 0.0 || shadow <= 0.0)
return 0.0;
N = normalize(N);
V = normalize(V);
L = normalize(L);
float NdotL = dot(N, L);
float NdotV = dot(N, V);
float NoL = saturate(dot(N, L));
float NoV = saturate(dot(N, V));
if (atten <= 0.0 || shadow <= 0.0)
if (NoL <= 1.0e-4 || NoV <= 1.0e-4)
return 0.0;
// Keep Doom/idTech's front-side behavior, but make the gate soft. A hard
// NdotL/NdotV cutoff was making normal-mapped and grazing surfaces randomly
// lose all specular even when the half-angle lobe should still be visible.
float lightFacing = smoothstep(-0.08, 0.18, NdotL);
float viewFacing = smoothstep(-0.04, 0.14, NdotV);
if (lightFacing <= 0.0 || viewFacing <= 0.0)
return 0.0;
// idTech4/Doom 3 interaction shader uses half-angle style specular,
// not the reflect(-L,N) Phong vector used in the old code here.
float3 H = Doom3SafeNormalizeOr(L + V, N);
float NdotH = saturate(dot(N, H));
float NoH = saturate(dot(N, H));
float VoH = saturate(dot(V, H));
float specTerm = Doom3SpecularLookup(NdotH) * lightFacing * viewFacing;
if (specTerm <= 1.0e-5)
if (NoH <= 1.0e-4 || VoH <= 1.0e-4)
return 0.0;
float3 specMask = saturate(specularAlbedo);
float specPeak = SpecularPeak3(specMask);
if (specPeak <= 0.001)
return 0.0;
// Doom 3's interaction pass is strongly additive. Keep it punchy,
// but clamp enough to avoid fireflies with stochastic light sampling.
const float DOOM3_SPECULAR_SCALE = 4.75;
float roughness = EstimateSpecularRoughness(specMask);
float a = roughness * roughness;
float a2 = max(a * a, 1.0e-4);
const float PI = 3.14159265;
// GGX/Trowbridge-Reitz distribution with Smith masking and Schlick Fresnel.
// This is still intentionally stylized for the existing idTech-style assets,
// but it produces more believable view-dependent highlights than the previous
// additive lookup lobe and does not alter light attenuation distance.
float dDenom = NoH * NoH * (a2 - 1.0) + 1.0;
float D = a2 / max(PI * dDenom * dDenom, 1.0e-4);
float k = ((roughness + 1.0) * (roughness + 1.0)) * 0.125;
float Gv = NoV / max(NoV * (1.0 - k) + k, 1.0e-4);
float Gl = NoL / max(NoL * (1.0 - k) + k, 1.0e-4);
float G = Gv * Gl;
float3 F0 = saturate(lerp(float3(0.025, 0.025, 0.025), specMask, 0.58));
float3 F = F0 + (1.0 - F0) * pow(1.0 - VoH, 5.0);
float specTerm = (D * G) / max(4.0 * NoL * NoV, 1.0e-4);
specTerm = min(specTerm, 5.0);
const float SPECULAR_ENERGY_SCALE = 2.35;
float3 specular =
lightColor *
lightIntensity *
atten *
shadow *
specMask *
NoL *
F *
specTerm *
DOOM3_SPECULAR_SCALE;
SPECULAR_ENERGY_SCALE;
return clamp(specular, 0.0, 8.0);
}
@@ -3768,17 +3816,16 @@ float3 PathTraceDirectPointLight(uint2 pixel, float3 worldPos, float3 N, float3
float3 L = toLight / dist;
float wrap = 0.28;
float NdotLWrap = saturate((dot(N, L) + wrap) / (1.0 + wrap));
float diffuseNoL = ComputeDiffuseLightingTerm(N, L);
float shadow = 1.0;
if (Lgt.samples != 0u && NdotLWrap > 0.0001)
if (Lgt.samples != 0u && diffuseNoL > 0.0001)
shadow = TraceVisibilityBiased(worldPos, N, L, dist);
if (Lgt.pointRadiusPad <= 0.5)
specAccum += ComputeSpecular(N, V, L, Lgt.color, Lgt.intensity, atten, shadow, specularAlbedo);
diffuseAccum += Lgt.color * (Lgt.intensity * atten * NdotLWrap * shadow);
diffuseAccum += Lgt.color * (Lgt.intensity * atten * diffuseNoL * shadow);
}
float invSamples = 1.0 / (float)sampleCount;
@@ -3798,17 +3845,16 @@ float3 PathTraceDirectSpotLight(float3 worldPos, float3 N, float3 V, float3 base
float3 L = toLight / dist;
float atten = ComputeSpotLightAttenuation(worldPos, Lgt);
float wrap = 0.28;
float NdotLWrap = saturate((dot(N, L) + wrap) / (1.0 + wrap));
float diffuseNoL = ComputeDiffuseLightingTerm(N, L);
float shadow = 1.0;
if (Lgt.samples != 0u && NdotLWrap > 0.0001 && atten > 0.0)
if (Lgt.samples != 0u && diffuseNoL > 0.0001 && atten > 0.0)
shadow = TraceVisibilityBiased(worldPos, N, L, dist);
if (Lgt.pointRadiusPad <= 0.5)
specularOut = ComputeSpecular(N, V, L, Lgt.color, Lgt.intensity, atten, shadow, specularAlbedo);
return Lgt.color * (Lgt.intensity * atten * NdotLWrap * shadow);
return Lgt.color * (Lgt.intensity * atten * diffuseNoL * shadow);
}
float3 PathTraceDirectRectLight(uint2 pixel, float3 worldPos, float3 N, float3 V, float3 baseAlbedo, float3 specularAlbedo, Light Lgt, inout uint rng, out float3 specularOut)
@@ -3852,7 +3898,7 @@ float3 PathTraceDirectRectLight(uint2 pixel, float3 worldPos, float3 N, float3 V
continue;
float3 L = sampleVec / sampleDist;
float NdotL = saturate(dot(N, L));
float NdotL = ComputeDiffuseLightingTerm(N, L);
if (NdotL <= 0.0)
continue;
@@ -4019,8 +4065,7 @@ float3 EstimateFastBounceLight(float3 hitPos, float3 hitN, Light Lgt)
if (atten <= 0.0)
return 0.0;
float wrap = 0.32;
float nDotL = saturate((dot(hitN, L) + wrap) / (1.0 + wrap));
float nDotL = ComputeDiffuseLightingTerm(hitN, L);
return clamp(Lgt.color * (Lgt.intensity * atten * nDotL), 0.0, 8.0);
}
else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_SPOT)
@@ -4035,8 +4080,7 @@ float3 EstimateFastBounceLight(float3 hitPos, float3 hitN, Light Lgt)
if (atten <= 0.0)
return 0.0;
float wrap = 0.32;
float nDotL = saturate((dot(hitN, L) + wrap) / (1.0 + wrap));
float nDotL = ComputeDiffuseLightingTerm(hitN, L);
return clamp(Lgt.color * (Lgt.intensity * atten * nDotL), 0.0, 8.0);
}
else if (Lgt.type == GL_RAYTRACING_LIGHT_TYPE_RECT)
@@ -4053,7 +4097,7 @@ float3 EstimateFastBounceLight(float3 hitPos, float3 hitN, Light Lgt)
return 0.0;
float3 L = toCenter / centerDist;
float nDotL = saturate(dot(hitN, L));
float nDotL = ComputeDiffuseLightingTerm(hitN, L);
if (nDotL <= 0.0)
return 0.0;
@@ -4507,8 +4551,8 @@ float3 PathTraceDeterministicLighting(
float3 skyColor = skyColorRGB * (0.35 + 0.65 * upness);
float3 lightingAccum = gAmbientColor.rgb * (gAmbientColor.a * 0.04);
lightingAccum += skyColor * (0.70 * skyVis);
lightingAccum += ambientSkyVis * (skyColorRGB * 0.15);
lightingAccum += skyColor * (0.42 * skyVis);
lightingAccum += ambientSkyVis * (skyColorRGB * 0.12);
// Glow-map emissive is direct/bloom-only for now. It is added after lighting
// in RayGen and is intentionally not injected into lightingAccum.
@@ -4805,6 +4849,24 @@ float3 EstimateRayTracedSpecularReflection(
}
)"
R"(
float3 ApplyRealisticOutputCurve(float3 color)
{
// Final photographic shoulder only: it does not change light radius or
// attenuation, but it prevents intense local lights/specular/bloom from
// clipping into a flat white patch. Values below 1.0 are left untouched.
color = max(color, 0.0);
float peak = max(max(color.r, color.g), color.b);
if (peak > 1.0)
{
float over = peak - 1.0;
float shoulderPeak = 1.0 + over / (1.0 + over * 0.38);
color *= shoulderPeak / max(peak, 1.0e-5);
}
return max(color, 0.0);
}
[shader("raygeneration")]
void RayGen()
{
@@ -4822,7 +4884,7 @@ void RayGen()
if (depthSample <= 0.0 || depthSample >= 1.0)
{
gOutputTex[pixel] = float4(albedoSample.rgb + emissiveSurface + emissiveBloom, albedoSample.a);
gOutputTex[pixel] = float4(ApplyRealisticOutputCurve(albedoSample.rgb + emissiveSurface + emissiveBloom), albedoSample.a);
return;
}
@@ -4840,7 +4902,7 @@ void RayGen()
if (isUnlit)
{
gOutputTex[pixel] = float4(baseAlbedo + emissiveSurface + emissiveBloom, albedoSample.a);
gOutputTex[pixel] = float4(ApplyRealisticOutputCurve(baseAlbedo + emissiveSurface + emissiveBloom), albedoSample.a);
return;
}
@@ -4851,8 +4913,8 @@ void RayGen()
// then reuse them for every stochastic GI sample.
float cavity = ComputeCavity(pixel, worldPos, N);
float ao = ComputeAmbientOcclusion(worldPos, N, pixel);
float skyVis = 0; //ComputeSkyVisibility(worldPos, N, pixel); // jmarshall - fix me later
float ambientSkyVis = TraceStraightUpToSky(worldPos, N);
float skyVis = 0; //ComputeSkyVisibility(worldPos, N, pixel);
float ambientSkyVis = 0; //TraceStraightUpToSky(worldPos, N);
float microShadow = lerp(0.75, 1.0, cavity);
float3 reactiveFinalGather = 0.0;
@@ -4947,7 +5009,7 @@ void RayGen()
// the eventual swap-chain/backbuffer is LDR.
finalColor += emissiveSurface + emissiveBloom;
gOutputTex[pixel] = float4(max(finalColor * ao, 0.0), albedoSample.a);
gOutputTex[pixel] = float4(ApplyRealisticOutputCurve(finalColor), albedoSample.a);
}
)";
+169 -1
View File
@@ -1012,12 +1012,16 @@ static const char* CAMWND_PROP_FILTER_MASK = "QER_CamWnd_FilterMask";
#define CAMWND_MENU_CMD_SHOW_ENTITIES 62104
#define CAMWND_MENU_CMD_SHOW_LIGHTS 62105
#define CAMWND_MENU_CMD_RESET_FILTERS 62106
#define CAMWND_MENU_CMD_SHOW_TARGET_LINES 62107
#define CAMWND_MENU_CMD_SHOW_TRIGGER_BRUSHES 62108
#define CAMWND_FILTER_HIDE_WORLD 0x00000001
#define CAMWND_FILTER_HIDE_PATCHES 0x00000002
#define CAMWND_FILTER_HIDE_MODELS 0x00000004
#define CAMWND_FILTER_HIDE_ENTITIES 0x00000008
#define CAMWND_FILTER_HIDE_LIGHTS 0x00000010
#define CAMWND_FILTER_HIDE_TARGET_LINES 0x00000020
#define CAMWND_FILTER_HIDE_TRIGGER_BRUSHES 0x00000040
static LRESULT CALLBACK CamWnd_MenuBarProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
@@ -1186,6 +1190,12 @@ static void CamWnd_HandleMenuCommand(CCamWnd* cam, int command) {
case CAMWND_MENU_CMD_SHOW_LIGHTS:
CamWnd_ToggleFilterBit(cam, CAMWND_FILTER_HIDE_LIGHTS);
break;
case CAMWND_MENU_CMD_SHOW_TARGET_LINES:
CamWnd_ToggleFilterBit(cam, CAMWND_FILTER_HIDE_TARGET_LINES);
break;
case CAMWND_MENU_CMD_SHOW_TRIGGER_BRUSHES:
CamWnd_ToggleFilterBit(cam, CAMWND_FILTER_HIDE_TRIGGER_BRUSHES);
break;
case CAMWND_MENU_CMD_RESET_FILTERS:
CamWnd_ResetFilters(cam);
break;
@@ -1241,6 +1251,9 @@ static void CamWnd_ShowFiltersMenu(HWND hWnd, CCamWnd* cam) {
CamWnd_AppendVisibleFilterItem(menu, mask, CAMWND_FILTER_HIDE_ENTITIES, CAMWND_MENU_CMD_SHOW_ENTITIES, "Show entities");
CamWnd_AppendVisibleFilterItem(menu, mask, CAMWND_FILTER_HIDE_LIGHTS, CAMWND_MENU_CMD_SHOW_LIGHTS, "Show lights");
AppendMenu(menu, MF_SEPARATOR, 0, NULL);
CamWnd_AppendVisibleFilterItem(menu, mask, CAMWND_FILTER_HIDE_TARGET_LINES, CAMWND_MENU_CMD_SHOW_TARGET_LINES, "Show actor/link lines");
CamWnd_AppendVisibleFilterItem(menu, mask, CAMWND_FILTER_HIDE_TRIGGER_BRUSHES, CAMWND_MENU_CMD_SHOW_TRIGGER_BRUSHES, "Show trigger/utility brushes");
AppendMenu(menu, MF_SEPARATOR, 0, NULL);
AppendMenu(menu, MF_STRING, CAMWND_MENU_CMD_RESET_FILTERS, "Reset filters");
command = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_LEFTALIGN | TPM_TOPALIGN, pt.x, pt.y, 0, hWnd, NULL);
@@ -1370,6 +1383,154 @@ static void CamWnd_UpdateRuntimeState(CCamWnd* cam, bool renderMode, bool rebuil
}
}
static bool CamWnd_StringStartsWithNoCase(const char* text, const char* prefix) {
if (text == NULL || prefix == NULL || prefix[0] == '\0') {
return false;
}
return idStr::Icmpn(text, prefix, (int)strlen(prefix)) == 0;
}
static bool CamWnd_StringContainsNoCase(const char* text, const char* needle) {
if (text == NULL || needle == NULL || needle[0] == '\0') {
return false;
}
const int needleLen = (int)strlen(needle);
for (const char* scan = text; *scan; scan++) {
if (idStr::Icmpn(scan, needle, needleLen) == 0) {
return true;
}
}
return false;
}
static bool CamWnd_MaterialPathHasSegment(const char* materialName, const char* segment) {
if (materialName == NULL || segment == NULL || segment[0] == '\0') {
return false;
}
const int segmentLen = (int)strlen(segment);
for (const char* scan = materialName; *scan; scan++) {
const bool segmentStart = (scan == materialName || scan[-1] == '/' || scan[-1] == '\\');
if (!segmentStart) {
continue;
}
if (idStr::Icmpn(scan, segment, segmentLen) != 0) {
continue;
}
const char end = scan[segmentLen];
if (end == '\0' || end == '/' || end == '\\') {
return true;
}
}
return false;
}
static bool CamWnd_IsUtilityMaterial(const idMaterial* material) {
if (material == NULL || material->GetName() == NULL || material->GetName()[0] == '\0') {
return false;
}
const char* materialName = material->GetName();
// Nodraw is the important case here and may appear outside textures/common
// in some projects, so match it anywhere in the material name.
if (CamWnd_StringContainsNoCase(materialName, "nodraw")) {
return true;
}
// The broader names below are only treated as utility when they live in a
// common/editor tool-material folder. This avoids hiding ordinary art
// materials that happen to contain words like "clip" or "portal".
if (!CamWnd_MaterialPathHasSegment(materialName, "common") &&
!CamWnd_MaterialPathHasSegment(materialName, "editor")) {
return false;
}
static const char* const utilityTokens[] = {
"caulk",
"trigger",
"clip",
"playerclip",
"monsterclip",
"moveclip",
"missileclip",
"aas",
"ladder",
"origin",
"portal",
"visportal",
"hint",
"skip",
"collision"
};
for (int i = 0; i < (int)(sizeof(utilityTokens) / sizeof(utilityTokens[0])); i++) {
if (CamWnd_StringContainsNoCase(materialName, utilityTokens[i])) {
return true;
}
}
return false;
}
static bool CamWnd_BrushUsesOnlyUtilityMaterials(brush_t* brush) {
if (brush == NULL) {
return false;
}
if (brush->pPatch) {
return CamWnd_IsUtilityMaterial(brush->pPatch->d_texture);
}
bool foundMaterial = false;
for (face_t* face = brush->brush_faces; face; face = face->next) {
if (face->face_winding == NULL) {
continue;
}
if (face->d_texture == NULL) {
return false;
}
foundMaterial = true;
if (!CamWnd_IsUtilityMaterial(face->d_texture)) {
return false;
}
}
return foundMaterial;
}
static bool CamWnd_IsTriggerLikeBrushEntity(brush_t* brush) {
if (brush == NULL || brush->owner == NULL) {
return false;
}
const char* className = ValueForKey(brush->owner, "classname");
if (className == NULL || className[0] == '\0') {
return false;
}
// These brush entities are editor/gameplay volumes rather than visible map
// geometry, so let the camera filter hide them independently from normal
// func_* brush models such as doors, movers, and func_static.
return CamWnd_StringStartsWithNoCase(className, "trigger") ||
CamWnd_StringStartsWithNoCase(className, "func_trigger") ||
CamWnd_StringStartsWithNoCase(className, "func_clip") ||
CamWnd_StringStartsWithNoCase(className, "func_playerclip") ||
CamWnd_StringStartsWithNoCase(className, "func_monsterclip") ||
CamWnd_StringStartsWithNoCase(className, "clip") ||
CamWnd_StringStartsWithNoCase(className, "playerclip") ||
CamWnd_StringStartsWithNoCase(className, "monsterclip");
}
static bool CamWnd_TargetLinesVisible(CCamWnd* cam) {
return (CamWnd_GetFilterMask(cam) & CAMWND_FILTER_HIDE_TARGET_LINES) == 0;
}
static bool CamWnd_MenuFilterBrush(CCamWnd* cam, brush_t* brush) {
if (!brush) {
return false;
@@ -1394,8 +1555,13 @@ static bool CamWnd_MenuFilterBrush(CCamWnd* cam, brush_t* brush) {
const bool isModel = (brush->modelHandle > 0) || (hasOwner && brush->owner->eclass->entityModel) || brush->entityModel;
const bool isBrushModel = hasOwner && IsBModel(brush);
const bool isFixedEntity = hasOwner && brush->owner->eclass->fixedsize;
const bool isTriggerLikeBrushEntity = CamWnd_IsTriggerLikeBrushEntity(brush);
const bool isUtilityMaterialBrush = CamWnd_BrushUsesOnlyUtilityMaterials(brush);
const bool isEntity = isLight || isModel || isBrushModel || isFixedEntity;
if ((mask & CAMWND_FILTER_HIDE_TRIGGER_BRUSHES) && (isTriggerLikeBrushEntity || isUtilityMaterialBrush)) {
return true;
}
if ((mask & CAMWND_FILTER_HIDE_LIGHTS) && isLight) {
return true;
}
@@ -2641,7 +2807,9 @@ void CCamWnd::Cam_Draw() {
// draw pointfile
glEnable(GL_DEPTH_TEST);
CamWnd_DrawVisiblePathLines();
if (CamWnd_TargetLinesVisible(this)) {
CamWnd_DrawVisiblePathLines();
}
if (g_qeglobals.d_pointfile_display_list) {
Pointfile_Draw();