Tutorial: Models and lighting Last edited 2 months ago2024-02-14 20:55:15 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 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.
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 going on? All surfaces that are affected by the light_environment will override any other light info. This is because of a global light value that's being stored in the game. This global light value has the same light colour and intensity as your light_environment. Whenever you move around on a surface that has a lightmap related to the light_environment, it'll use this global value to light the model.

How do we fix this issue? The ZHLT.fgd has 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. Make sure you set 'Is Sky' to 'Yes'.
User posted image
The light_spot lights your outside area just like the light_environment does but it doesn't tie those faces to the global light value. 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 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 Tree's.
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 7 months ago2023-09-18 21:06:00 UTC Comment #105568
Another great guide, Hezus! Thanks for making this!
Commented 7 months 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 4 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 3 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.

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