Pimp my model

New cool client effects for 3D models

Columbia Pictures logo

Hexen II uses dozens of 3D models for players, weapons, monsters, projectiles, goodies, puzzle pieces, decorations & props of all sorts. Some have very cool client effects attached to them:
  • Pickups and puzzle pieces await floating in the air and spinning.
  • Mana pickups and torches feature a colored glow around them.
  • Most projectiles feature a trail of particles.
  • Some even cast light dynamically on the geometry around.
Unfortunately all those special effects are hard-coded, either in the models' mdl files by means of special flags, or even directly in the game engine (after the mdl file name). For that reason it was not possible for a mapper to have any control.

But that was before.

I am extremely proud to introduce the new addition I made to the game engine to allow an easy and fine control over such effects for any model, and most importantly the new entity I designed for that purpose.

misc_modelpimp

Here is the entity definition (put it in your fgd file to see it in TrenchBroom):
@PointClass base(Appearflags, Targetname) color(255 170 255) model ({"path": model}) = misc_modelpimp : "Model properties"
[
	model(string) : "Model to pimp"
	spawnflags(flags) =
	[
		1: "Spin" : 0
		2: "Float" : 0
		4: "Glow orb" : 0
		8: "Cast light" : 0
	]
	flags(flags) =
	[
		1: "Rocket smoke trail" : 0
		2: "Grenade smoke trail" : 0
		4: "Gib long blood trail" : 0
		8: "Rotate" : 0
		16: "Scrag double green trail" : 0
		32: "Zombie gib short blood trail" : 0
		64: "Hellknight orange split trail" : 0
		128: "Vore purple trail" : 0
		256: "Fireball" : 0
		512: "Ice trail" : 0
		1024: "Mipmap" : 0
		2048: "Ink spit" : 0
		4096: "Transparent sprite" : 0
		8192: "Vertical spray" : 0
		16384: "Holey (fence) texture" : 0
		32768: "Translucent" : 0
		65536: "Always facing" : 0
		131072: "Bottom & top trail" : 0
		262144: "Slow staff move" : 0
		524288: "Blue/white magic drip" : 0
		1048576: "Bone shards drip" : 0
		2097152: "Scarab dust" : 0
		4194304: "Acid ball" : 0
		8388608: "Blood rain" : 0
		16777216: "Far mipmap" : 0
	]
	glow_color(string) : "Color for the glow and/or illumination"
	abslight(integer)  : "Glow alpha transparency" : 0.75
	view_ofs(string)   : "Glow offset relatively to model origin (x y z)" : "0 0 0"
	health(integer)    : "Glow radius" : 20
	max_health(integer): "Cast light radius" : 200
	wait(integer)      : "Refresh rate" : 0.5
	style(choices) : "Style (32-63 for groups)" : 0 =
	[
		0 : "Normal"
		1 : "Soft flicker"
		6 : "Faster flicker"

		4 : "Low falloff"

		5 : "Pulse"
		2 : "Slow pulse in & out"
		11 : "Quick pulse in & out"

		3 : "Erratic pulse & flicker A"
		7 : "Erratic pulse & flicker B"
		8 : "Erratic pulse & flicker C"
		9 : "Slow blink"
		10 : "Fluorescent flicker"
		
		12 : "Custom style 1"
		13 : "Custom style 2"
		14 : "Custom style 3"
		15 : "Custom style 4"
		16 : "Custom style 5"
		17 : "Custom style 6"
		18 : "Custom style 7"
		19 : "Custom style 8"
		20 : "Custom style 9"
		21 : "Custom style 10"
		22 : "Custom style 11"
		23 : "Custom style 12"
		24 : "Custom style 13"
		
		25 : "MLS_FULLBRIGHT"
		26 : "MLS_POWERMODE"
		27 : "MLS_TORCH"
		28 : "MLS_FIREFLICKER"
		29 : "MLS_CRYSTALGOLEM"
	]
]
  • model is the path of the model you want to pimp (like "models/stool.mdl" for example). It's important to understand that all occurrences of that model in the map will be impacted, because misc_modelpimp applies at model level, not at entity level.

  • spawnflags let you define whether the model spins and/or floats like puzzle pieces traditionally do. Usually the two effects come together and cannot be chosen apart one from each other; now they can. spawnflags also lets you define whether the model has a glow orb like mana and torches, and whether it casts light (in which case it's a client-side dynamic light like the torch artefact, not precalculated environmental light processed by light.exe).

  • flags corresponds to all the preset effects you may set up in a model editor like qME to hard code special behaviors. Those flags are pretty much undocumented except for the very first ones dating back to Quake and still supported by Hexen II (even if with occasional modifications; for example the scrags' double green trail is not green anymore due to palette difference). The list above is therefore the best and most comprehensive documentation available about those flags. All flags were undocumented but available in qME 2.x but the later qME 3.x only exposes the original Quake flags, which is sad news for us Hexen II modders. Hence my decision of supporting them in misc_modelpimp.
    Please note that most flags are for projectile trails, that is moving models. For that reason they don't produce any visual outcome if used with static models. Make sure to use them on projectiles, monsters (weird but it works!), trains, etc.

    Some worth mentioning non-trail values are
    • "Rotate", the usual setting for puzzle pieces and pickups, reponsible for the model both floating and spinnning.
    • "Holey" for models whose skin should have transparent pixels (I used it for the firsherman's net in my Ice Lake map).
    • "Always facing" for a model always facing the player just as sprites usually do (can't see any use for it but well...).

    Because that's really a lot of settings to figure out, misc_modelpimp features a special showcase mode (see below).

  • glow_color applies if spawnflags "Glow orb" or "Cast light" are set. It defines the color for the glowing orb and/or light cast by the model.

  • abslight goes hand in hand with glow_color and defines its alpha value.

  • view_ofs is an offset vector added to the model's origin to neatly position the glow (such an offset is applied in vanilla Hexen II so that the torch glow is located on the flame instead of in the middle of the torch wooden handle).

  • health is the radius of the glowing orb (if any).

  • max_health is the radius of the cast light (if any).

  • wait is not something you should need to modify but you can read Under the hood for more advanced questions.

  • style is the well-known setting usually found in light entities, telling whether the light should be static, blinking, pulsating, etc. Here it applies to the glowing orb and/or cast light. The proposed values not only stick to the usual ones but also make clear which slots are available for user-defined custom light styles and which slots are reserved by the game for special uses, starting from style 25.

  • targetname is not used normally. Having the misc_modelpimp entity present in the map is enough for it to automatically do its job. Yet some special uses of targetname are discussed in the next sections.

Columbia Pictures logo recreation in Hexen II
Now you should pretty much figure out how I made this remake of this article's feature image...

Showcase mode

All the available settings in flags sound extremely appealling. But testing them all one after each other is obviously far less appealling. That's why misc_modelpimp's showcase mode is for.
Give the entity a targetname then call it by means of a button, for example. Each time the entity is used, it shows the effect of a different flag and even displays an in-game plaque mentioning the name of the current setting. It starts from "Rocket smoke trail" and advances one step farther upon each call up to "Far mipmap" then comes back to the beginning. This mode is obviously not designed for a real in-game context, but allows to quickly browse the preset trails library and pick up your favorite. You can then check its value for good in the flags property (whose value is ignored when in showcase mode).

Under the hood

As mentioned earlier, misc_modelpimp operates at model level, not at entity level.

The first consequence is that all entities sharing the same model will be equally affected by the customizations made by misc_modelpimp. Which means not only every instance of a given entity using this model, but really every instance of every entity using this model. Granted, entities with different classnames don't use to share a common model... but it happens. For example, if you want a puzzle_piece surrounded by a magical glow, please note that the corresponding puzzle_static_piece will feature the same effect since it uses the same model. Or if you want ice imps followed by an ice trail, which makes sense, unfortunately your fire imps will too because they are just a different skin of the same model.
Now, nothing prevents you from copypasting the mdl file and giving the copy another name like "iceimp.mdl". Then pimp that model only with misc_modelpimp, and the fire imps will go on behaving as normal (or will feature their own fire trail if you prefer). Of course it requires some HexenC editing to tell the ice imp to use "iceimp.mdl" instead of the usual "imp.mdl".
Also, the scope of misc_modelpimp's customizations is the map where it stands. When traveling to another map, the player won't see the special effects anymore, unless a copy of the same misc_modelpimp is there too. It's a minor annoyance and conversely allows for special maps where the usual rules are a bit changed. For example imagine a Halloween secret map where torches glow green instead of yellow, whereas they look normal everywhere else in the hub...

The second and most upsetting consequence is that misc_modelpimp's customizations are never saved by the game engine. It's done for entities to retrieve their previous state when the player comes back to an already visited map or loads a saved game. But models are NOT entities and are NOT supposed to be dynamically pimped at execution time through HexenC. For that reason, and after weeks of trying to figure out a more elegant solution, I resorted to making misc_modelpimp a cyclic entity regularly resending its directives to the game engine every wait seconds, just in case the player just reloaded the map and lost the previous customizations. 99.999% of the time it's useless and a waste of CPU but well... Please do feel free to propose a better implementation, I'll really appreciate!
This poor technique has as least the advantage of working since the misc_modelpimp state is duly saved (as a regular entity), even if its effects are not. wait is set to 0.5 second by default which is the amount of time during which you'll see yellow torches before they switch to green when entering the hypothetic Halloween map. So take wait as misc_modelpimp's reaction time. You can adjust it to a lesser value but then it will spam the game engine at an even higher rate to reapply the same settings again and again.

And now the entity's code. Please credit me (and let me know ) if you use it in your progs:
/*QUAKED misc_modelpimp by Inky (11/2021)
An entity able to pimp the properties of a model (like rotation, glow, dynamic light, etc.)
flags	ignore the hard coded flags in the mdl file and use those instead
		Caution : the value set in flags is ignored if the "Showcase mode" is active because of "using" the entity
		          if so the flags value is handled by code automatically and each call to .use
				  applies a different flag value for flags testing purpose
*/
string FlagFriendlyNames[25] =
{
	"_Rocket smoke trail",
	"_Grenade smoke trail",
	"_Gib long blood trail",
	"_Rotate",
	"_Scrag double green trail",
	"_Zombie gib short blood trail",
	"_Hellknight orange split trail",
	"_Vore purple trail",
	"_Fireball",
	"_Ice trail",
	"_Mipmap",
	"_Ink spit",
	"_Transparent sprite",
	"_Vertical spray",
	"_Holey (fence) texture",
	"_Translucent",
	"_Always facing player",
	"_Bottom & top trail",
	"_Slow staff move",
	"_Blue/white magic drip",
	"_Bone shards drip",
	"_Scarab dust",
	"_Acid ball",
	"_Blood rain",
	"_Far mipmap"
};

float modelpimp_showcase()
{
	//Advance the flags for the next call
	if(self.flags == 16777216)
	{
		self.walkframe = 0;
		self.flags = 1; //We've come full circle, back to the beginning
	}
	else
	{
		self.walkframe = self.walkframe + 1;
		if(self.flags == 0) self.flags = 1; else self.flags = self.flags * 2; //Next value 
	}
	entity player = find (world, classname, "player");
	centerprint(player,FlagFriendlyNames[self.walkframe]);

	return pimpmodel(self, self.glow_color);
}

void modelpimp_think ()
{
	pimpmodel(self, self.glow_color);
	
	//Do it again later (a small delay is mandatory when (re)loading a map; trying to pimp the model right away would come too early and fail)
	self.nextthink = time + self.wait;
}

void() misc_modelpimp =
{
	precache_model(self.model);
	setmodel(self, self.model);
	self.effects(+)EF_NODRAW;
	
	if(!self.wait) self.wait = 0.5;
	
	if(self.targetname)
	{
		self.use = modelpimp_showcase;
		self.walkframe = -1;
		self.flags = 0;
	}
	
	self.think = modelpimp_think;
	self.nextthink = time + self.wait;
}
The new pimpmodel function has to be referenced in builtin.hc:
float pimpmodel(entity reference, vector glow_color) : 111;
And the new glow_color property must be declared in entity.hc:
.vector glow_color;

Going even farther

A last and very special use of misc_modelpimp is worth mentioning.

As explained above, targetname's main use if for the showcase mode. That special mode is actually activated by giving the misc_modelpimp entity a targetname and triggering it. Not by JUST giving a targetname.

Now what is the point of having a targetname if you don't trigger it? Think of the targetname as an anchor giving you the ability to get a reference on an entity. The code having this reference can now manipulate the referenced entity and change its properties. Vanilla Hexen II does so with trigger_activate switching the active/inactive state of its targeted entities without triggering them, or with trigger_message_transfer retargeting its targeted entities by giving their target property a new value, also without triggering them.

With that in mind, a new custom entity could perfectly find a misc_modelpimp instance thanks to its targetname, then dynamically modify its values (like switching a trail on/off, increasing the glow size or changing its color, whatever) and the change would take effect as soon as wait seconds later, thanks to misc_modelpimp's cyclic nature. In which case it may make sense to temporarily lesser the wait value for a more responsive change.

Want to ask for clarification, report an issue with this trick or propose another one? Drop me an email If you use the trick please credit me and put a link to this website.