Tutorial: Models and lighting Last edited 2 months ago2024-09-15 09:56:48 UTC

This tutorial goes into the workings and quirks surrounding model lighting in Half-Life.

In GoldSource (the Half-Life engine), models will only receive one type of lighting. Generally the model grabs its lighting info from the brush below the origin point of the model.
User posted image
User posted image
Monsters such as the Barnacle use the internal EF_INVLIGHT flag, which inverts the direction of the lighting. The Barnacle will look for its lighting info above the origin point. As you can see from the screenshot below, the barnacle is lit up in green and it'll ignore the blue light below it.

This flag can be given to most entities by adding the effects KV with a value of 16 (if the effects KV already exists, add 16 to its current value). This flag can be useful for models such as chandeliers.
User posted image
Lighting info is grabbed from any surface that holds lightmap data. This includes all solid world geometry, but not brush entities, such as func_wall, func_breakable, func_train, etc. Even though those brush entities are affected by lighting, the model lighting will ignore these surfaces and instead look for the next available light info on a world geometry face.

In this example, the surface the player is standing on is clearly blue, but the model is lit red because of the red light below this func_wall.
User posted image
To fix this, you either have to revert the brushes back to world geometry or turn them into func_detail. Func_detail technically is not a brush entity, so the model will have no issues taking light info from these surfaces.
User posted image
Model lighting in outside areas has some special rules. Here is an outside area lit by a light_environment. As you can see, the light in the bright area and in the shadows affect our model in different ways, as expected.
User posted image
User posted image
Things become strange once we add more light sources. The blue point light is completely ignored by the model.
User posted image
What's happening here is that entities always check if they can see the sky (i.e. a surface with SKY texture) from a certain direction i.e. the direction of skylight, which light_environment sets using a set of CVARs (sv_skyvec_* for X,Y,Z of a unit vector). If they can, then they will override their lighting with the calculated sky colour, which light_environment also sets in another set of CVARs (sv_skycolor_* for R,G,B). Basically, if entities can see the sky from the skylight's angle, they will be lit exclusively by skylight.

How do we fix this issue? VHLT ships with its ZHLT.fgd a new entity called info_sunlight. With this you can set a custom global light value. You could set this and adjust the model lighting. However, there is a better and easier way.

Instead of a light_environment, you can use a light_spot, with 'Is Sky' to 'Yes'.
User posted image
The light_spot lights your outside area just like the light_environment does but since it's not a light_environment it won't override model lighting. Therefore it'll use the actual light info below the model again.
User posted image
Another issue with outside areas is the SKY texture. This texture does not hold any lightmap info. If any model is above it, it'll not receive any light at all.
User posted image
There is a workaround for this issue. You can create a BLACK_HIDDEN brush. Make a new brush with SKIP on all sides and BLACK_HIDDEN on the top surface. These are tool textures found in zhlt.wad. Place this brush above your SKY texture and turn it into func_detail. Make sure it is non-solid: Passable = Yes (zhlt_noclip 1).
User posted image
BLACK_HIDDEN is an invisible brush that will retain lightmap info and enables you to have proper light on models again.
User posted image
These BLACK_HIDDEN brushes are also great in places where model lighting is unrealistic, such as grates. These are func_wall, so models will not get their light info from the func_wall surface but from the light below in the pit (where it is much darker). By putting a BLACK_HIDDEN brush on top of this grate, you can fix this issue.
User posted image
User posted image
User posted image
You could also place these BLACK_HIDDEN brushes in long elevator shafts to keep updating the player model lighting. Or put them across a chasm or broken bridge where players have to jump across. If used with User posted image SKIP and zhlt_noclip 1, this brush cannot affect players and monsters so you can place as many as you like.

BLACK_HIDDEN can also work great for Ospreys and Apaches to make sure they get the right amount of light. In this case, I'm using a large brush to cover the entire area where the Osprey is flying. This makes sure it will not render in full black when it passes over a SKY texture. It will also prevent it from picking up unwanted light info from any other structures on the ground. If you use large surfaces like these. be sure to scale up the BLACK_HIDDEN texture (10 or 20 scale) to improve compile times and lighten the load on lightmap resources.
User posted image
If your Osprey or Apache still renders fully black, it might be too high up. The engine will stop looking for lightmap info once a model is higher than 2048 units above a surface.
User posted image
If you want a model to have specific lighting info, you can use the zhlt_copylight value. Go into the properties of the model you want to adjust, disable SmartEdit and add the keyvalue zhlt_copylight and a targetname (for example: light_info). Then create an info_target entity, name it light_info and place it somewhere where you'd like to copy the lightmap info from. The model will then copy a small patch of that lightmap under its origin point. This can be very useful for static models such a Xen Trees, or prop models.
User posted image
I hope this tutorial helped you understand model lighting and lightmaps better. May you now create properly lit maps and models!

Misc info:

4 Comments

Commented 1 year ago2023-09-18 21:06:00 UTC Comment #105568
Another great guide, Hezus! Thanks for making this!
Commented 1 year ago2023-09-18 21:44:53 UTC Comment #105569
Worth noting that black_hidden is allegedly drawn in Sven Co-op, according to this wiki's Tool textures page. I haven't personally verified this; it's probably either an issue with SCHLT or that Sven assumes lightmapped surfaces are drawn.
Commented 11 months ago2023-12-15 18:01:48 UTC Comment #105729
@sirYodaJedi:
In SC the black_hidden texture is not drawn. It's also hidden.
Commented 11 months ago2023-12-31 19:52:10 UTC Comment #105822
From reading the code it seems that sv_skyvector_* and sv_skycolor_* are the culprits of models forcibly taking the sky's color, because from looking at texlight data itself there's nothing to discern sky light from normal light. The engine probably trace a vector set in sv_skyvector_* from the origin (feet) and if it hits the sky texture, overrides the model's lighting with the sv_skycolor_*.

sv_skycolor_* is read from the brightness value of light_environment at map load, and sv_skyvector_* from the entity's angle also at map load.
[2024-09-12] I think I solved the black_hidden debacle. It's hidden as world brush BUT visible when made part of a brush entity. the folks who saw the texture probably didn't realize that it won't work on entities.
[2024-09-15] A primer about origin would be good. Models whose origin are inside the walls won't get lighting and be rendered black, and a way to shift it with QC and HLAM. Additionally, a way to have wall-mounted models take lighting from the walls instead of the floor when you shine a flashlight around, for example. I touched on the latter in my tutorial about model health/hev chargers.

You must log in to post a comment. You can login or register a new account.