diff --git a/src/cgame/default/cg_entity_effect.c b/src/cgame/default/cg_entity_effect.c index 2f9aad573..0456a11d1 100644 --- a/src/cgame/default/cg_entity_effect.c +++ b/src/cgame/default/cg_entity_effect.c @@ -192,7 +192,7 @@ void Cg_EntityEffects(cl_entity_t *ent, r_entity_t *e) { .origin = e->origin, .radius = ent->current.termination.x, .color = Color32_Color(ent->current.color).vec3, - .intensity = 1.f, + .intensity = ent->current.termination.y ?: 1.f, .source = ent }); } diff --git a/src/cgame/default/cg_flare.c b/src/cgame/default/cg_flare.c index a6139d1b4..8f15ed27c 100644 --- a/src/cgame/default/cg_flare.c +++ b/src/cgame/default/cg_flare.c @@ -21,6 +21,37 @@ #include "cg_local.h" +/** + * @brief A binding from a `SURF_TOGGLE` surface (material stage or flare) to the + * `target_light` or styled `light` that controls its brightness. + */ +typedef struct { + + /** + * @brief True if a controlling light was resolved. Unbound surfaces render normally. + */ + _Bool found; + + /** + * @brief True if the controller is a `target_light` (switched on/off at runtime via + * `EF_LIGHT`); false for a static `light` (always on, brightness from its style). + */ + _Bool switchable; + + /** + * @brief The controlling light's origin, used to resolve its live entity for the + * `EF_LIGHT` switch state (switchable lights only). + */ + vec3_t origin; + + /** + * @brief The controlling light's `a`-`z` style string (may be empty) and phase offset, + * driving the animated brightness via `Cg_AnimateLight`. + */ + char style[MAX_BSP_ENTITY_VALUE]; + float drift; +} cg_light_binding_t; + /** * @brief The flare type. */ @@ -50,12 +81,133 @@ typedef struct { * @brief The entity referencing the model containin this flare, if any. */ const cl_entity_t *entity; + + /** + * @brief Binding to a controlling light for `SURF_TOGGLE` flares. The flare's alpha is + * scaled by the light's brightness (on/off and/or style). Unbound if `light.found` is false. + */ + cg_light_binding_t light; } cg_flare_t; static Vector *cg_flares; #define FLARE_ALPHA_RAMP 0.01 +/** + * @brief Returns true if a live `target_light` at the given origin is currently lit. + */ +static _Bool Cg_ToggleLightLit(const vec3_t origin) { + + const cl_entity_t *e = cgi.client->entities; + for (int32_t i = 0; i < MAX_ENTITIES; i++, e++) { + + // a target_light that has switched off is culled from the snapshot, but its + // last-seen state lingers in the persistent entities array. Only trust an + // entity that was actually transmitted in the current frame, matching how the + // renderer adds the dynamic light (Cg_AddEntities iterates the frame only). + if (e->frame_num != cgi.client->frame.frame_num) { + continue; + } + + if (!(e->current.effects & EF_LIGHT)) { + continue; + } + + if (Vec3_Distance(e->current.origin, origin) < 1.f) { + return true; + } + } + + return false; +} + +/** + * @brief Binds a `SURF_TOGGLE` surface to the nearest light controlling the given material. + * @details Both switched `target_light` entities (matched in the entity list, animated by their + * live `EF_LIGHT` state) and static styled `light` entities (matched in the baked BSP lights, + * which carry the team-resolved, phase-correct style) are candidates; the nearest match wins. + * @param material The material name to match against light `material` keys. + * @param pos The surface position to measure proximity from. + * @param b Filled with the resolved binding; `b->found` is false if no controlling light exists. + */ +static void Cg_BindToggleLight(const char *material, const vec3_t pos, cg_light_binding_t *b) { + + memset(b, 0, sizeof(*b)); + + const r_bsp_model_t *bsp = cgi.WorldModel()->bsp; + const cm_bsp_t *cm = bsp->cm; + + float best = FLT_MAX; + + // switched lights: target_light entities, animated at runtime by EF_LIGHT + for (int32_t i = 0; i < cm->num_entities; i++) { + const cm_entity_t *e = cm->entities[i]; + + const char *classname = cgi.EntityValue(e, "classname")->nullable_string; + if (!classname || q_strcmp(classname, "target_light")) { + continue; + } + + const char *m = cgi.EntityValue(e, "material")->nullable_string; + if (!m || q_strcmp(m, material)) { + continue; + } + + const vec3_t o = cgi.EntityValue(e, "origin")->vec3; + const float d = Vec3_Distance(o, pos); + if (d < best) { + best = d; + b->found = true; + b->switchable = true; + b->origin = o; + const char *style = cgi.EntityValue(e, "style")->nullable_string; + q_strlcpy(b->style, style ?: "", sizeof(b->style)); + b->drift = cgi.EntityValue(e, "drift")->value; + } + } + + // static styled lights: baked BSP lights carry the resolved style/drift (target_lights + // are not baked, so there is no overlap with the loop above) + const r_bsp_light_t *l = bsp->lights; + for (int32_t i = 0; i < bsp->num_lights; i++, l++) { + + if (!l->entity) { + continue; + } + + const char *m = cgi.EntityValue(l->entity, "material")->nullable_string; + if (!m || q_strcmp(m, material)) { + continue; + } + + const float d = Vec3_Distance(l->origin, pos); + if (d < best) { + best = d; + b->found = true; + b->switchable = false; + b->origin = l->origin; + q_strlcpy(b->style, l->style, sizeof(b->style)); + b->drift = l->drift; + } + } +} + +/** + * @brief Returns the current brightness `[0, 1]` of a toggle light binding: a switched + * `target_light` contributes its on/off state, and any light style modulates on top, so a + * plain switched light is binary and a styled light flickers. Unbound bindings return 1. + */ +static float Cg_ToggleLightAlpha(const cg_light_binding_t *b) { + + if (!b->found) { + return 1.f; + } + + const float base = b->switchable ? (Cg_ToggleLightLit(b->origin) ? 1.f : 0.f) : 1.f; + + return base * Cg_AnimateLight(1.f, b->style, b->drift); +} + /** * @brief Adds all loaded flare sprites to the view, attenuated by their surface angle to the camera. */ @@ -68,6 +220,13 @@ void Cg_AddFlares(void) { for (size_t i = 0; i < cg_flares->count; i++) { cg_flare_t *flare = VectorValue(cg_flares, cg_flare_t *, i); + // a toggle flare's brightness tracks its controlling light (on/off and/or style); + // unbound flares return 1 and are unaffected + const float toggle_alpha = Cg_ToggleLightAlpha(&flare->light); + if (toggle_alpha <= 0.f) { + continue; + } + mat4_t matrix = Mat4_Identity(); flare->entity = NULL; @@ -114,7 +273,7 @@ void Cg_AddFlares(void) { // Dot product gives us facing: positive=front, negative=back const float dot = Vec3_Dot(Vec3_Direction(cgi.view->origin, flare->out.origin), plane.normal); // Use absolute value to allow sprites from behind, but abs(dot) reduces visibility for grazing angles - const float alpha = Clampf01(Maxf(fabsf(dot), 0.25f) * cg_add_flares->value); + const float alpha = Clampf01(Maxf(fabsf(dot), 0.25f) * cg_add_flares->value) * toggle_alpha; if (alpha == 0.f) { continue; @@ -246,6 +405,18 @@ void Cg_LoadFlares(void) { Cg_MergeFlares(); + // bind flares on SURF_TOGGLE faces to their nearest controlling light; the flare then + // tracks that light's brightness. Scoping is by the face flag alone (not the `toggle` + // stage keyword), so unrelated faces using the same material keep their always-on flares. + for (size_t i = 0; i < cg_flares->count; i++) { + cg_flare_t *flare = VectorValue(cg_flares, cg_flare_t *, i); + + if (flare->face->brush_side->surface & SURF_TOGGLE) { + const char *material = flare->face->brush_side->material->cm->name; + Cg_BindToggleLight(material, flare->in.origin, &flare->light); + } + } + Cg_Debug("Loaded %zu flares\n", cg_flares->count); } @@ -259,3 +430,87 @@ void Cg_FreeFlares(void) { cg_flares = NULL; } } + +/** + * @brief A `SURF_TOGGLE` draw element driven by a controlling light. + */ +typedef struct { + + /** + * @brief The controlled draw element. The renderer reads `draw->toggle_alpha`. + */ + r_bsp_draw_elements_t *draw; + + /** + * @brief The binding to the controlling light, resolved at load. + */ + cg_light_binding_t light; +} cg_material_toggle_t; + +static Vector *cg_material_toggles; + +/** + * @brief Binds each `SURF_TOGGLE` draw element to the nearest light whose `material` key + * matches the element's material. Each element is driven independently, so panels sharing + * one material can chase or flicker individually. Elements default to fully on (`toggle_alpha` + * 1); only bound elements are dimmed. + */ +void Cg_LoadMaterialToggles(void) { + + cg_material_toggles = $(alloc(Vector), initWithSize, sizeof(cg_material_toggle_t)); + + r_bsp_model_t *bsp = cgi.WorldModel()->bsp; + + for (int32_t i = 0; i < bsp->num_draw_elements; i++) { + r_bsp_draw_elements_t *draw = &bsp->draw_elements[i]; + + draw->toggle_alpha = 1.f; + + if (!(draw->surface & SURF_TOGGLE)) { + continue; + } + + cg_material_toggle_t toggle = { .draw = draw }; + Cg_BindToggleLight(draw->material->cm->name, Box3_Center(draw->bounds), &toggle.light); + if (!toggle.light.found) { + continue; + } + + $(cg_material_toggles, add, &toggle); + } + + Cg_Debug("Loaded %zu material toggles\n", cg_material_toggles->count); +} + +/** + * @brief Frees the material toggle controller list. + */ +void Cg_FreeMaterialToggles(void) { + + if (cg_material_toggles) { + release(cg_material_toggles); + cg_material_toggles = NULL; + } +} + +/** + * @brief Refreshes each controlled draw element's `toggle_alpha` from its controlling + * light's current brightness (on/off state and/or light style). + */ +void Cg_UpdateMaterialToggles(void) { + + if (!cg_material_toggles) { + return; + } + + // nothing reads toggle_alpha while material stages aren't being drawn, so don't + // bother resolving controlling lights (the flare path is gated by cg_add_flares) + if (!cgi.GetCvarValue("r_draw_material_stages")) { + return; + } + + for (size_t i = 0; i < cg_material_toggles->count; i++) { + cg_material_toggle_t *toggle = VectorElement(cg_material_toggles, cg_material_toggle_t, i); + toggle->draw->toggle_alpha = Cg_ToggleLightAlpha(&toggle->light); + } +} diff --git a/src/cgame/default/cg_flare.h b/src/cgame/default/cg_flare.h index 7c7f808c6..4887e85c0 100644 --- a/src/cgame/default/cg_flare.h +++ b/src/cgame/default/cg_flare.h @@ -27,4 +27,7 @@ void Cg_AddFlares(void); void Cg_LoadFlares(void); void Cg_FreeFlares(void); +void Cg_LoadMaterialToggles(void); +void Cg_FreeMaterialToggles(void); +void Cg_UpdateMaterialToggles(void); #endif /* __CG_LOCAL_H__ */ diff --git a/src/cgame/default/cg_light.c b/src/cgame/default/cg_light.c index ba782a453..01d0da1af 100644 --- a/src/cgame/default/cg_light.c +++ b/src/cgame/default/cg_light.c @@ -122,6 +122,93 @@ float Cg_AnimateLight(float intensity, const char *style, float drift) { return intensity; } +/** + * @brief Stable pseudo-random phase for a stage instance. Mirrors the renderer's + * `R_StageDriftHash` (same pointer mix) so a pulse-driven light stays in phase with the + * glow stage on the same draw element. + */ +static float Cg_StageDriftHash(const void *a, const void *b) { + uint32_t h = (uint32_t) ((uintptr_t) a >> 4) ^ (uint32_t) ((uintptr_t) b >> 4); + h ^= h >> 16; + h *= 0x7feb352dU; + h ^= h >> 15; + h *= 0x846ca68bU; + h ^= h >> 16; + return h / (float) UINT32_MAX; +} + +/** + * @brief Returns the material's first `pulse` stage, or `NULL` if it has none. + */ +static const r_stage_t *Cg_MaterialPulseStage(const r_material_t *material) { + + for (const r_stage_t *stage = material->stages; stage; stage = stage->next) { + if (stage->cm->flags & STAGE_PULSE) { + return stage; + } + } + + return NULL; +} + +/** + * @brief Drives static, styleless `light` entities from a controlling material's `pulse` + * stage, so a pulsing glow (e.g. a jump pad) makes the light it casts breathe in sync. + * @details A light opts in with a `material` key; it then binds to the nearest draw element + * using that material that has a `pulse` stage, and inherits that stage's frequency and + * (per-panel) phase. Lights with a `style` are skipped - their style already animates them. + * The match is by material name only (no `SURF_TOGGLE` needed: such glows are always-on). + */ +void Cg_LoadLightPulses(void) { + + r_bsp_model_t *bsp = cgi.WorldModel()->bsp; + + r_bsp_light_t *l = bsp->lights; + for (int32_t i = 0; i < bsp->num_lights; i++, l++) { + + l->pulse_hz = 0.f; + l->pulse_drift = 0.f; + + if (!l->entity || *l->style) { + continue; + } + + const char *material = cgi.EntityValue(l->entity, "material")->nullable_string; + if (!material) { + continue; + } + + float best = FLT_MAX; + const r_bsp_draw_elements_t *best_draw = NULL; + const r_stage_t *best_stage = NULL; + + for (int32_t j = 0; j < bsp->num_draw_elements; j++) { + const r_bsp_draw_elements_t *draw = &bsp->draw_elements[j]; + + if (q_strcmp(draw->material->cm->name, material)) { + continue; + } + + const r_stage_t *stage = Cg_MaterialPulseStage(draw->material); + if (!stage) { + continue; + } + + const float d = Vec3_Distance(l->origin, Box3_Center(draw->bounds)); + if (d < best) { + best = d; + best_draw = draw; + best_stage = stage; + } + } + + if (best_draw) { + l->pulse_hz = best_stage->cm->pulse.hz; + l->pulse_drift = best_stage->cm->pulse.drift * Cg_StageDriftHash(best_draw, best_stage); + } + } +} + /** * @brief Resolves the model1 index for a BSP inline model string (e.g. "*3"). * @return The model1 index, or -1 if not found. @@ -147,7 +234,14 @@ static void Cg_AddBspLights(void) { r_bsp_light_t *l = cgi.WorldModel()->bsp->lights; for (int32_t i = 0; i < cgi.WorldModel()->bsp->num_lights; i++, l++) { - const float intensity = Cg_AnimateLight(l->intensity ?: 1.f, l->style, l->drift); + float intensity = Cg_AnimateLight(l->intensity ?: 1.f, l->style, l->drift); + + // a pulse-driven light breathes in sync with its controlling glow stage; this is the + // same sine the shader applies to the stage (shared clock via cgi.view->ticks) + if (l->pulse_hz) { + const float t = cgi.view->ticks * .001f; + intensity *= (sinf((t + l->pulse_drift) * l->pulse_hz * (float) M_PI) + 1.f) * .5f; + } if (l->target_entity) { // Resolve the inline model string and find the matching cl_entity_t each frame. diff --git a/src/cgame/default/cg_light.h b/src/cgame/default/cg_light.h index 3e7dfde70..98da7693f 100644 --- a/src/cgame/default/cg_light.h +++ b/src/cgame/default/cg_light.h @@ -79,6 +79,7 @@ typedef struct { float Cg_AnimateLight(float intensity, const char *style, float drift); void Cg_AddLight(const cg_light_t *s); void Cg_AddLights(void); +void Cg_LoadLightPulses(void); void Cg_InitLights(void); void Cg_FreeLights(void); diff --git a/src/cgame/default/cg_main.c b/src/cgame/default/cg_main.c index 1c6ef1795..8a6a82100 100644 --- a/src/cgame/default/cg_main.c +++ b/src/cgame/default/cg_main.c @@ -393,6 +393,8 @@ static void Cg_PopulateScene(const cl_frame_t *frame) { Cg_AddEffects(); + Cg_UpdateMaterialToggles(); + Cg_AddFlares(); Cg_AddSprites(); diff --git a/src/cgame/default/cg_media.c b/src/cgame/default/cg_media.c index fdf1f7b5f..c7bdb5fd8 100644 --- a/src/cgame/default/cg_media.c +++ b/src/cgame/default/cg_media.c @@ -337,6 +337,10 @@ void Cg_LoadMedia(void) { Cg_LoadFlares(); + Cg_LoadMaterialToggles(); + + Cg_LoadLightPulses(); + cgi.LoadingProgress(-1, "entities"); Cg_LoadEntities(); @@ -376,6 +380,8 @@ void Cg_FreeMedia(void) { Cg_FreeFlares(); + Cg_FreeMaterialToggles(); + Cg_FreeSprites(); cgi.FreeTag(MEM_TAG_CGAME); diff --git a/src/client/renderer/r_bsp_draw.c b/src/client/renderer/r_bsp_draw.c index b75de19d7..697b30a5c 100644 --- a/src/client/renderer/r_bsp_draw.c +++ b/src/client/renderer/r_bsp_draw.c @@ -180,12 +180,22 @@ static void R_DrawBspDrawElementsMaterialStage(const r_view_t *view, const r_entity_t *entity, const r_bsp_draw_elements_t *draw, const r_material_t *material, - const r_stage_t *stage) { - - glUniform1i(r_bsp_program.stage.flags, stage->cm->flags); - - if (stage->cm->flags & STAGE_COLOR) { - glUniform4fv(r_bsp_program.stage.color, 1, stage->cm->color.rgba); + const r_stage_t *stage, + float modulate) { + + if (modulate < 1.f) { + // a controlling light dims this stage: scale its alpha so the emissive + // contribution tracks the light's brightness. Force STAGE_COLOR so the + // shader applies the (possibly synthesized) color even for plain stages. + color_t color = (stage->cm->flags & STAGE_COLOR) ? stage->cm->color : color_white; + color.a *= modulate; + glUniform1i(r_bsp_program.stage.flags, stage->cm->flags | STAGE_COLOR); + glUniform4fv(r_bsp_program.stage.color, 1, color.rgba); + } else { + glUniform1i(r_bsp_program.stage.flags, stage->cm->flags); + if (stage->cm->flags & STAGE_COLOR) { + glUniform4fv(r_bsp_program.stage.color, 1, stage->cm->color.rgba); + } } if (stage->cm->flags & STAGE_PULSE) { @@ -310,13 +320,29 @@ static void R_DrawBspDrawElementsMaterialStages(const r_view_t *view, glActiveTexture(GL_TEXTURE0 + TEXTURE_STAGE); + // On a SURF_TOGGLE draw element, a controlling light scales the light-driven stages + // by draw->toggle_alpha (the cgame writes the light's brightness each frame). If the + // material marks specific stages with the `toggle` keyword, only those are driven; + // otherwise every drawable stage is. The base diffuse is a separate pass, so it is + // never affected. toggle_alpha defaults to 1, so unbound surfaces render normally. + const _Bool light_driven = (draw->surface & SURF_TOGGLE); + const _Bool driven_stages_only = (material->cm->stage_flags & STAGE_TOGGLE); + for (r_stage_t *stage = material->stages; stage; stage = stage->next) { if (!(stage->cm->flags & STAGE_DRAW)) { continue; } - R_DrawBspDrawElementsMaterialStage(view, entity, draw, material, stage); + float modulate = 1.f; + if (light_driven && (!driven_stages_only || (stage->cm->flags & STAGE_TOGGLE))) { + modulate = draw->toggle_alpha; + if (modulate <= 0.f) { + continue; // fully off; nothing to draw + } + } + + R_DrawBspDrawElementsMaterialStage(view, entity, draw, material, stage, modulate); } glUniform1i(r_bsp_program.stage.flags, STAGE_NONE); diff --git a/src/client/renderer/r_sprite.c b/src/client/renderer/r_sprite.c index dcfa96377..bdfd13b8f 100644 --- a/src/client/renderer/r_sprite.c +++ b/src/client/renderer/r_sprite.c @@ -49,6 +49,9 @@ static struct { GLint texture_voxel_light_data; GLint texture_voxel_light_indices; + GLint texture_voxel_occlusion; + GLint texture_voxel_caustics; + GLint texture_sky; GLint texture_depth_attachment_copy; @@ -480,7 +483,7 @@ static void R_InitSpriteProgram(void) { memset(&r_sprite_program, 0, sizeof(r_sprite_program)); r_sprite_program.name = R_LoadProgram( - R_ShaderDescriptor(GL_VERTEX_SHADER, "material.glsl", "voxel.glsl", "sprite_vs.glsl", NULL), + R_ShaderDescriptor(GL_VERTEX_SHADER, "material.glsl", "voxel.glsl", "light.glsl", "sprite_vs.glsl", NULL), R_ShaderDescriptor(GL_FRAGMENT_SHADER, "soften_fs.glsl", "sprite_fs.glsl", NULL), NULL); @@ -498,12 +501,18 @@ static void R_InitSpriteProgram(void) { r_sprite_program.texture_next_diffusemap = glGetUniformLocation(r_sprite_program.name, "texture_next_diffusemap"); r_sprite_program.texture_voxel_light_data = glGetUniformLocation(r_sprite_program.name, "texture_voxel_light_data"); r_sprite_program.texture_voxel_light_indices = glGetUniformLocation(r_sprite_program.name, "texture_voxel_light_indices"); + r_sprite_program.texture_voxel_occlusion = glGetUniformLocation(r_sprite_program.name, "texture_voxel_occlusion"); + r_sprite_program.texture_voxel_caustics = glGetUniformLocation(r_sprite_program.name, "texture_voxel_caustics"); + r_sprite_program.texture_sky = glGetUniformLocation(r_sprite_program.name, "texture_sky"); r_sprite_program.texture_depth_attachment_copy = glGetUniformLocation(r_sprite_program.name, "texture_depth_attachment_copy"); glUniform1i(r_sprite_program.texture_diffusemap, TEXTURE_DIFFUSEMAP); glUniform1i(r_sprite_program.texture_next_diffusemap, TEXTURE_NEXT_DIFFUSEMAP); glUniform1i(r_sprite_program.texture_voxel_light_data, TEXTURE_VOXEL_LIGHT_DATA); glUniform1i(r_sprite_program.texture_voxel_light_indices, TEXTURE_VOXEL_LIGHT_INDICES); + glUniform1i(r_sprite_program.texture_voxel_occlusion, TEXTURE_VOXEL_OCCLUSION); + glUniform1i(r_sprite_program.texture_voxel_caustics, TEXTURE_VOXEL_CAUSTICS); + glUniform1i(r_sprite_program.texture_sky, TEXTURE_SKY); glUniform1i(r_sprite_program.texture_depth_attachment_copy, TEXTURE_DEPTH_ATTACHMENT_COPY); glUseProgram(0); diff --git a/src/client/renderer/r_types.h b/src/client/renderer/r_types.h index 7f276dc8f..402b5e90f 100644 --- a/src/client/renderer/r_types.h +++ b/src/client/renderer/r_types.h @@ -563,6 +563,15 @@ typedef struct { * @brief Texture coordinate origin for stage transforms (scale, stretch, rotate). */ vec2_t st_origin; + + /** + * @brief Runtime brightness for `SURF_TOGGLE` draw elements, in `[0, 1]`. The controlling + * light's current brightness (a `target_light`'s on/off state and/or a light style's `a`-`z` + * animation) scales the element's light-driven material stages. `0` suppresses them entirely + * (equivalent to the old off state), `1` is full. Defaults to `1` (rendered normally until a + * controlling light drives it); driven each frame by the cgame. + */ + float toggle_alpha; } r_bsp_draw_elements_t; /** @@ -851,6 +860,15 @@ typedef struct { */ float drift; + /** + * @brief Runtime drive (set by the cgame at load): when non-zero, this light's intensity is + * modulated by a `pulse` sine matching a controlling material's glow stage on a `SURF_TOGGLE` + * surface, so the cast light breathes in sync with the glow. `pulse_hz` 0 = no pulse drive; + * `pulse_drift` is the matching phase offset (already multiplied by the stage drift hash). + */ + float pulse_hz; + float pulse_drift; + /** * @brief True if this light's shadowmap can be reused from the previous frame. */ diff --git a/src/client/renderer/shaders/sprite_vs.glsl b/src/client/renderer/shaders/sprite_vs.glsl index 1c21d033b..e5b2b9552 100644 --- a/src/client/renderer/shaders/sprite_vs.glsl +++ b/src/client/renderer/shaders/sprite_vs.glsl @@ -35,21 +35,10 @@ out vertex_data { } vertex; /** - * @brief - */ -vec3 sprite_lighting_light(in int index) { - - light_t light = lights[index]; - - float dist = distance(light.origin.xyz, in_position); - float radius = light.origin.w; - float atten = clamp(1.0 - dist / radius, 0.0, 1.0); - - return light_color(light) * atten; -} - -/** - * @brief Dynamic lighting for sprites + * @brief Lights the sprite using the shared voxel lighting model from light.glsl + * (ambient + occlusion + exposure + per-light diffuse + caustics), blended by the + * sprite's lighting weight. Billboards have no surface normal, so a camera-facing + * normal is synthesized for `vertex_light`'s lambert term and the sky sample. */ void sprite_lighting(void) { @@ -57,28 +46,22 @@ void sprite_lighting(void) { return; } - vec3 diffuse = vec3(0.0); - - if (editor == 0) { - ivec3 voxel = voxel_xyz(in_position); - ivec2 data = voxel_light_data(voxel); + common_vertex_t v; + v.model_position = in_position; - for (int i = 0; i < data.y; i++) { - int index = voxel_light_index(data.x + i); - diffuse += sprite_lighting_light(index); - } - } + // camera world position = -R^T * t from the (orthonormal) view matrix + vec3 camera = -transpose(mat3(view)) * view[3].xyz; + v.model_normal = normalize(camera - in_position); - for (int i = 0; i < MAX_DYNAMIC_LIGHTS; i++) { - int index = active_lights[i]; - if (index == -1) { - break; - } + v.voxel = voxel_uvw(in_position); + v.ambient = vec3(0.0); + v.diffuse = vec3(0.0); + v.caustics = 0.0; - diffuse += sprite_lighting_light(index); - } + vertex_lighting(v); - vertex.color.rgb = mix(vertex.color.rgb, vertex.color.rgb * diffuse, in_lighting); + vec3 lit = vertex.color * (v.ambient + v.diffuse); + vertex.color = mix(vertex.color, lit, in_lighting); } /** @@ -94,8 +77,6 @@ void main(void) { vertex.color = in_color; vertex.lerp = in_lerp; - vec3 texcoord = voxel_uvw(in_position); - sprite_lighting(); gl_Position = projection3D * view * position; diff --git a/src/collision/cm_material.c b/src/collision/cm_material.c index f33deb5c1..2615b9730 100644 --- a/src/collision/cm_material.c +++ b/src/collision/cm_material.c @@ -104,6 +104,7 @@ static cm_dictionary_t cm_surfaceList[] = { { .keyword = "alpha_test", .flag = SURF_ALPHA_TEST }, { .keyword = "phong", .flag = SURF_PHONG }, { .keyword = "material", .flag = SURF_MATERIAL }, + { .keyword = "toggle", .flag = SURF_TOGGLE }, }; /** @@ -552,6 +553,11 @@ static bool Cm_ParseStage(cm_material_t *m, cm_stage_t *s, parser_t *parser) { continue; } + if (!q_strcmp(token, "toggle")) { + s->flags |= STAGE_TOGGLE; + continue; + } + if (*token == '}') { // a texture or envmap mean draw it @@ -1235,6 +1241,10 @@ static void Cm_WriteStage(const cm_material_t *material, const cm_stage_t *stage Fs_Print(file, "\t\tshell %0.2f\n", stage->shell.radius); } + if (stage->flags & STAGE_TOGGLE) { + Fs_Print(file, "\t\ttoggle\n"); + } + Fs_Print(file, "\t}\n"); } diff --git a/src/collision/cm_material.h b/src/collision/cm_material.h index de8ce9a6a..528cbecd5 100644 --- a/src/collision/cm_material.h +++ b/src/collision/cm_material.h @@ -260,6 +260,8 @@ typedef enum { STAGE_FLARE = (1 << 19), STAGE_SHELL = (1 << 20), + STAGE_TOGGLE = (1 << 21), + STAGE_DRAW = (1 << 30), } cm_stage_flags_t; diff --git a/src/game/default/g_entity_target.c b/src/game/default/g_entity_target.c index 98161f27c..504d41f1f 100644 --- a/src/game/default/g_entity_target.c +++ b/src/game/default/g_entity_target.c @@ -21,29 +21,69 @@ #include "g_local.h" -#define LIGHT_START_ON 1 +#define LIGHT_START_ON 1 +#define LIGHT_BACK_AND_FORTH 2 /** - * @brief For singular lights, simply toggle them. For teamed lights, - * advance through the team, toggling two at a time. + * @brief Returns the team member immediately preceding `node`, or the master + * itself if `node` is the first member. The team is a singly-linked list, so we + * simply walk it from the head; chains are short, so this is cheap. + */ +static g_entity_t *G_target_light_Prev(g_entity_t *master, const g_entity_t *node) { + + g_entity_t *prev = master; + while (prev->team_next && prev->team_next != node) { + prev = prev->team_next; + } + return prev; +} + +/** + * @brief For singular lights, simply toggle them. For teamed lights, advance the + * lit member through the team, one lit at a time. By default the chase wraps from + * the tail back to the master; with the `back_and_forth` flag it instead reverses + * direction at each end, bouncing the lit member back and forth. The direction is + * stored on the master in `count` (0 = forward, 1 = backward). */ static void G_target_light_Cycle(g_entity_t *ent) { g_entity_t *master = ent->team_master; - if (master) { - G_Debug("Cycling %s\n", etos(master->enemy)); + if (!master || master->team_next == NULL) { + // no team, or a team of one: just toggle + ent->s.effects ^= EF_LIGHT; + return; + } - master->enemy->s.effects ^= EF_LIGHT; - master->enemy = master->enemy->team_next; + G_Debug("Cycling %s\n", etos(master->enemy)); + + master->enemy->s.effects ^= EF_LIGHT; + + if (master->spawn_flags & LIGHT_BACK_AND_FORTH) { + + if (master->count == 0) { // forward + if (master->enemy->team_next) { + master->enemy = master->enemy->team_next; + } else { // reached the tail, reverse + master->count = 1; + master->enemy = G_target_light_Prev(master, master->enemy); + } + } else { // backward + if (master->enemy != master) { + master->enemy = G_target_light_Prev(master, master->enemy); + } else { // reached the head, reverse + master->count = 0; + master->enemy = master->team_next; + } + } + } else { + master->enemy = master->enemy->team_next; if (master->enemy == NULL) { master->enemy = master; } - - master->enemy->s.effects ^= EF_LIGHT; - } else { - ent->s.effects ^= EF_LIGHT; } + + master->enemy->s.effects ^= EF_LIGHT; } /** @@ -64,12 +104,15 @@ static void G_target_light_Use(g_entity_t *ent, g_entity_t *other, g_entity_t *a } } -/*QUAKED target_light (1 1 1) (-4 -4 -4) (4 4 4) start_on +/*QUAKED target_light (1 1 1) (-4 -4 -4) (4 4 4) start_on back_and_forth Emits a user-defined light when used. Lights can be chained with teams. -------- Keys -------- color : The light color (default 1.0 1.0 1.0). radius : The radius of the light in units (default 300). + intensity : The light brightness multiplier, matching point lights (default 1.0). + material : The name of a material whose `toggle` stages (and flares) switch with this + light, e.g. quake2/ceil2_base. Apply the `toggle` surface flag to the faces. delay : The delay before activating, in seconds (default 0). targetname : The target name of this entity. team : The team name for alternating lights. @@ -77,6 +120,8 @@ static void G_target_light_Use(g_entity_t *ent, g_entity_t *other, g_entity_t *a -------- Spawn flags -------- start_on : The light will start on. + back_and_forth : Teamed lights bounce the lit member back and forth instead of + wrapping from the tail back to the master. Set on the master. -------- Notes -------- Use this entity to add switched lights. Use the wait key to synchronize @@ -92,8 +137,12 @@ void G_target_light(g_entity_t *ent) { float radius = gi.EntityValue(ent->def, "radius")->value; radius = radius ?: 300.f; + float intensity = gi.EntityValue(ent->def, "intensity")->value; + intensity = intensity ?: 1.f; + ent->s.color = Color_Color32(Color3fv(color)); ent->s.termination.x = radius; + ent->s.termination.y = intensity; if (ent->spawn_flags & LIGHT_START_ON) { ent->s.effects |= EF_LIGHT; diff --git a/src/quemap/map.c b/src/quemap/map.c index 5f2c62837..76b9a9ddb 100644 --- a/src/quemap/map.c +++ b/src/quemap/map.c @@ -445,6 +445,15 @@ static void SetMaterialFlags(brush_side_t *side) { if (side->contents & CONTENTS_MASK_LIQUID) { side->surface |= SURF_LIQUID; } + + // a `toggle` surface switches its material's drawable stages and flares with a light; + // if the material has neither, there is nothing to switch. Warn so mappers aren't left + // wondering why a target_light does nothing. (A `toggle` stage keyword is optional - it + // only restricts which stages are driven when several are present.) + if ((side->surface & SURF_TOGGLE) && !(material->cm->stage_flags & (STAGE_DRAW | STAGE_FLARE))) { + Com_Warn("Material \"%s\" has the toggle surface flag but no drawable or flare stages " + "to switch\n", side->texture); + } } /** diff --git a/src/quemap/writebsp.c b/src/quemap/writebsp.c index 99f037e95..71a114827 100644 --- a/src/quemap/writebsp.c +++ b/src/quemap/writebsp.c @@ -532,8 +532,9 @@ static int32_t FaceCmp(const void * a, const void * b) { order = a_surface - b_surface; if (order == 0) { - if (a_surface & SURF_MATERIAL) { - // Brush side faces with SURF_MATERIAL are unique per brush side + if (a_surface & (SURF_MATERIAL | SURF_TOGGLE)) { + // SURF_MATERIAL and SURF_TOGGLE faces are unique per brush side, so that + // each switchable surface is an independently addressable draw element. return a_face->brush_side - b_face->brush_side; } } diff --git a/src/quetoo.h b/src/quetoo.h index bf4d0d1bd..abbee29e9 100644 --- a/src/quetoo.h +++ b/src/quetoo.h @@ -240,6 +240,7 @@ typedef enum { #define SURF_ALPHA_TEST 0x400 // alpha test (grates, fences, foliage, etc..) #define SURF_PHONG 0x800 // phong interpolated lighting at compile time #define SURF_MATERIAL 0x1000 // skip diffuse pass, draw material stages only +#define SURF_TOGGLE 0x2000 // material stages switched on/off by a target_light #define SURF_BEVEL 0x20000000 // brush side is a bevel with approximate material #define SURF_NODE 0x40000000 // brush side is a node splitter with no material