Rendu 3D et effets

Côté client : placer un texte au-dessus d'un joueur, dessiner des panneaux dans le monde (3D2D), afficher des images et ses propres polices, entourer des objets (halos), tracer des formes de débogage, dessiner faisceaux, sprites et maillages en 3D (avec stencils), créer des particules, des effets Lua (EFFECT) et des décalcomanies, allumer des lumières dynamiques et des projecteurs, teinter, flouter ou styliser l'écran, et afficher des modèles visibles par un seul joueur.

Client uniquementTout ce qui suit n'existe que dans les scripts client (CLIENT == true : cl_init.lua, client/, lua/autorun/client/). Rien n'est envoyé par le réseau : chaque joueur ne voit que ce que son Lua client dessine. Pour montrer un effet à tous, le serveur envoie un message net et chaque client le dessine.

Hooks de rendu

HookArgumentsQuandUsage
PreRender—Une fois par image, en premierPréparer l'image (calculs, positions)
PreDrawHalos—Une fois par image, avant le renduAppeler halo.Add
RenderScreenspaceEffects—Une fois par imageDrawColorModify, DrawBloom…
PostRender—Une fois par image, après les précédentsFin de l'image
PostDrawOpaqueRenderablesfalse, falseUne fois par image, pendant le dessin des panneaux 3D2Dcam.Start3D2D, render.Draw*, faisceaux, sprites, mesh
PostDrawTranslucentRenderablesfalse, falseJuste après le précédentIdem (compatibilité GMod)

Les deux arguments (bDrawingDepth, bDrawingSkybox dans GMod) valent toujours false. Le HUD 2D reste dessiné dans HUDPaint.

Ordre dans une image : PreRender → début du dessin 3D de l'image (les particules des ParticleEmitter bougent, les EFFECT font Think puis Render, les DynamicLight sont placées) → PreDrawHalos → RenderScreenspaceEffects → PostRender → puis, pendant le dessin de l'interface, PostDrawOpaqueRenderables et PostDrawTranslucentRenderables.

Position à l'écran : ToScreen

vec:ToScreen() → table donne { x, y, visible, onscreen } :

MursToScreen ne tient pas compte des murs : un nom peut s'afficher à travers un mur. Combine-le avec une trace si besoin.
-- Nom et santé au-dessus de la tête des autres joueurs.
hook.Add("HUDPaint", "NomsAuDessus", function()
	local moi = LocalPlayer()
	for _, ply in ipairs(player.GetAll()) do
		if ply ~= moi and ply:Alive() and ply:GetPos():Distance(moi:GetPos()) < 25 then
			local s = (ply:GetPos() + Vector(0, 2.1, 0)):ToScreen()
			if s.onscreen then
				draw.SimpleText(ply:Nick(), "Bold", s.x, s.y, Color(255, 255, 255), TEXT_ALIGN_CENTER, TEXT_ALIGN_BOTTOM)
				draw.SimpleText(ply:Health() .. " PV", "Small", s.x, s.y + 2, Color(120, 255, 120), TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP)
			end
		end
	end
end)

Dessiner dans le monde : cam.Start3D2D

cam.Start3D2D(pos, ang, échelle) … cam.End3D2D(), uniquement dans le hook PostDrawOpaqueRenderables (ou PostDrawTranslucentRenderables) ; ailleurs, une erreur s'affiche une fois. Entre les deux, draw.SimpleText, draw.RoundedBox, surface.DrawRect, surface.DrawText, surface.DrawTexturedRect… dessinent sur un panneau placé dans le monde au lieu du HUD.

ArgumentRôle
posPosition dans le monde du point (0, 0) du dessin, qui est le centre du panneau
angOrientation en degrés (comme ent:GetAngles()). L'axe x du dessin suit l'axe X local, l'axe y du dessin descend (−Y local). Le panneau n'est visible que du côté +Z local (de dos, il est invisible). Vector(0, 0, 0) : panneau vertical, lisible par quelqu'un placé du côté +Z. Vector(-90, 0, 0) : posé à plat, lisible d'en haut.
échelleMètres par pixel (défaut 0,01 : 100 px = 1 m ; de 0,0005 à 1)

Pour tourner le panneau vers un point cible : local d = cible - pos puis Vector(0, math.deg(math.atan(d.x, d.z)), 0) (en Lua 5.4, math.atan2 n'existe pas : math.atan(y, x) prend deux arguments).

-- Prix affiché au-dessus des entités de la classe « vente ».
surface.CreateFont("PrixGros", { font = "Barlow", size = 64, weight = 700 })

hook.Add("PostDrawOpaqueRenderables", "PanneauxPrix", function()
	local oeil = LocalPlayer():EyePos()
	for _, ent in ipairs(ents.FindByClass("vente")) do
		local pos = ent:GetPos() + Vector(0, 1.2, 0)
		if (pos - oeil):Length() < 15 then
			local d = oeil - pos
			local ang = Vector(0, math.deg(math.atan(d.x, d.z)), 0) -- face au joueur
			cam.Start3D2D(pos, ang, 0.005)
				draw.RoundedBox(12, -160, -50, 320, 100, Color(20, 20, 20, 220))
				draw.SimpleText(ent:GetNWInt("prix", 0) .. " $", "PrixGros", 0, 0, Color(255, 210, 60), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
			cam.End3D2D()
		end
	end
end)
Limites du 3D2D
  • Zone dessinable : de −512 à +512 px autour de (0, 0) sur chaque axe. Ce qui dépasse est coupé (et peut déborder sur le panneau voisin). Avec l'échelle 0,01, cela fait un carré de 10,24 m.
  • 16 panneaux au plus par image (au-delà : erreur affichée une fois, panneaux ignorés). Le 5e, le 9e et le 13e agrandissent la texture interne et n'apparaissent qu'à l'image suivante (un clignotement à la première apparition). Mémoire vidéo : 16 Mo pour 1 à 4 panneaux, jusqu'à 64 Mo pour 16.
  • La résolution est fixe (1 pixel = échelle mètres) : pour un texte net de près, prends une petite échelle (0,005) et une grande police.
  • Les panneaux sont cachés par le décor et les objets, et toujours en pleine lumière (non éclairés), comme dans GMod.
  • Appeler cam.Start3D2D alors qu'un panneau est ouvert ferme le précédent. Ce qui est dessiné hors d'un couple Start3D2D / End3D2D dans ces hooks n'apparaît nulle part.

Images : Material

Material(chemin) → matériau charge une image d'un dossier materials/ du gamemode ou d'un addon activé ("mon_addon/logo.png"), ou une image intégrée : "physicore/logo.png", "physicore/icon_physgun.png", "physicore/icon_toolgun.png", et pour les particules et sprites "physicore/particle_glow.png" (halo lumineux), "physicore/particle_smoke.png" (fumée), "physicore/particle_spark.png" (étincelle). Comme pour les sons, les autres joueurs ne voient que les images du jeu et du Workshop (celles d'un addon local restent chez l'hôte). Formats : .png, .jpg, .jpeg, .webp, .svg (4096 px au plus). Image introuvable : erreur affichée et :IsError() vaut true. Crée le matériau une fois (hors de HUDPaint) et garde-le dans une variable.

FonctionRôle
mat:IsError(), mat:Width(), mat:Height(), mat:GetName()Erreur ? Taille en pixels (0 si erreur), chemin
surface.SetMaterial(mat)Image des fonctions suivantes (accepte aussi un chemin texte)
surface.NoTexture()Plus d'image : les fonctions dessinent un rectangle plein
surface.DrawTexturedRect(x, y, l, h)L'image étirée dans le rectangle, teintée par surface.SetDrawColor (blanc = couleurs d'origine, l'alpha la rend transparente)
surface.DrawTexturedRectRotated(x, y, l, h, angle)(x, y) est le centre ; angle en degrés, sens inverse des aiguilles d'une montre
surface.DrawTexturedRectUV(x, y, l, h, u0, v0, u1, v1)Une partie de l'image (coordonnées 0 à 1, (0, 0) = coin haut gauche)
surface.DrawPoly(points)Polygone plein : { {x = …, y = …, u = …, v = …}, … } (3 à 256 points ; u, v de 0 à 1 dans l'image, 0 par défaut), de la couleur de surface.SetDrawColor, avec l'image de surface.SetMaterial s'il y en a une (après surface.NoTexture() : couleur unie)

Ces fonctions marchent dans HUDPaint et dans un panneau 3D2D. Les panneaux DImage affichent aussi des images dans les fenêtres.

local logo = Material("mon_gamemode/logo.png")
local fleche = Material("mon_gamemode/fleche.png")

hook.Add("HUDPaint", "Logo", function()
	if logo:IsError() then return end
	surface.SetDrawColor(255, 255, 255, 230)
	surface.SetMaterial(logo)
	surface.DrawTexturedRect(20, 20, logo:Width() / 4, logo:Height() / 4)
	-- La flèche tourne avec le regard du joueur.
	local aim = LocalPlayer():GetAimVector()
	surface.SetMaterial(fleche)
	surface.DrawTexturedRectRotated(ScrW() - 60, 60, 64, 64, math.deg(math.atan(aim.x, -aim.z)))
end)

Polices : surface.CreateFont

surface.CreateFont(nom, options) crée (ou remplace) une police utilisable partout où un nom de police est demandé (draw.SimpleText, draw.GetFontHeight, surface.SetFont, SetFont des panneaux…). 128 polices au plus ; elles sont effacées quand les scripts sont rechargés.

OptionRôle
font"Barlow" (défaut), "Barlow Condensed", ou un fichier .ttf / .otf / .woff / .woff2 cherché dans resource/fonts/ puis fonts/ du gamemode (et de ses bases), puis des addons activés ("MaPolice.ttf", "resource/fonts/MaPolice.ttf" ou "addon:<clé>/fonts/MaPolice.ttf" ; 16 Mo au plus). Les polices Windows courantes de GMod ("Roboto", "Tahoma", "Arial", "Verdana", "Coolvetica"…) donnent Barlow ; un nom inconnu sans extension donne Barlow avec un message d'erreur.
sizeTaille en pixels (18 par défaut, de 4 à 256)
weightGraisse (500 par défaut). Barlow : < 600 normal, 600–699 demi-gras, ≥ 700 gras. Barlow Condensed : demi-gras ou gras (≥ 700). Fichier personnel : ≥ 600 ajoute un gras synthétique
italictrue : penché (synthétique)
outline, shadowContour noir, ombre portée : appliqués seulement par surface.DrawText (draw.SimpleText ajoute toujours son léger contour)

Écrire avec surface

FonctionRôle
surface.SetFont(nom), surface.GetFont()Police de DrawText et GetTextSize
surface.GetTextSize(texte)Deux valeurs : largeur, hauteur en pixels (texte sur plusieurs lignes accepté)
surface.SetTextColor(r, g, b, a) ou (Color)Couleur du texte
surface.SetTextPos(x, y), surface.GetTextPos()Coin haut gauche du prochain texte ; GetTextPos renvoie {x, y}
surface.DrawText(texte)Dessine puis avance x de la largeur du texte (on peut enchaîner plusieurs couleurs)
-- Bandeau d'argent en deux couleurs.
surface.CreateFont("Argent", { font = "Barlow Condensed", size = 40, weight = 700, outline = true })

hook.Add("HUDPaint", "Argent", function()
	local argent = LocalPlayer():GetNWInt("argent", 0)
	surface.SetFont("Argent")
	local l1 = surface.GetTextSize("Argent : ")
	local l2, h = surface.GetTextSize(argent .. " $")
	local x = ScrW() - l1 - l2 - 30
	draw.RoundedBox(8, x - 10, 20, l1 + l2 + 20, h + 10, Color(0, 0, 0, 160))
	surface.SetTextPos(x, 25)
	surface.SetTextColor(255, 255, 255)
	surface.DrawText("Argent : ")
	surface.SetTextColor(Color(120, 255, 120))
	surface.DrawText(argent .. " $")
end)

Contours : halo.Add

halo.Add(entités, couleur, flou_x, flou_y, passes, additif, à_travers_murs) (seuls les deux premiers sont obligatoires), à appeler chaque image depuis le hook PreDrawHalos : le contour ne dure que l'image en cours.

-- Contour sur le prop visé et sur les coéquipiers.
hook.Add("PreDrawHalos", "Contours", function()
	local tr = LocalPlayer():GetEyeTrace()
	if tr.Entity and IsValid(tr.Entity) and not tr.Entity:IsPlayer() and tr.HitPos:Distance(tr.StartPos) < 8 then
		halo.Add({ tr.Entity }, Color(255, 200, 0), 2)
	end
	local amis = {}
	for _, ply in ipairs(player.GetAll()) do
		if ply ~= LocalPlayer() and ply:Team() == LocalPlayer():Team() then amis[#amis + 1] = ply end
	end
	halo.Add(amis, Color(80, 160, 255, 200), 1, 1, 1, true, true)
end)

Formes de débogage : render.Draw*

Formes 3D valables pour l'image en cours : à appeler chaque image depuis PostDrawOpaqueRenderables (depuis Think, elles clignoteraient). Non éclairées, couleurs avec alpha, 60 000 sommets au plus par image.

FonctionForme
render.DrawLine(a, b, couleur, profondeur)Segment. profondeur (false par défaut, comme writeZ de GMod) : false = toujours visible par-dessus tout, true = caché par le décor
render.DrawBox(pos, ang, mins, maxs, couleur)Boîte pleine (cachée par le décor), coins mins / maxs dans le repère (pos, ang), faces visibles des deux côtés
render.DrawWireframeBox(pos, ang, mins, maxs, couleur, profondeur)Arêtes de la boîte (profondeur : false par défaut)
render.DrawSphere(pos, rayon, couleur) ou (pos, rayon, pas_longitude, pas_latitude, couleur)Sphère pleine cachée par le décor (16 × 10 par défaut, de 4 à 32 et de 3 à 32)
-- Visualiser la zone d'un objectif.
hook.Add("PostDrawOpaqueRenderables", "ZoneObjectif", function()
	local centre = GetGlobalVector("objectif", Vector(0, 0, 0))
	render.DrawWireframeBox(centre, Vector(0, 0, 0), Vector(-3, 0, -3), Vector(3, 2, 3), Color(255, 80, 80), true)
	render.DrawSphere(centre + Vector(0, 2.5, 0), 0.3, Color(255, 80, 80, 150))
	render.DrawLine(LocalPlayer():GetPos(), centre, Color(255, 255, 255, 120))
end)

Dessin 3D de l'image : faisceaux, sprites, quads

Comme dans GMod, on peut dessiner des formes texturées dans le monde : faisceaux (lasers, éclairs), sprites toujours face à la caméra (lueurs), quadrilatères et maillages. Ce dessin ne vaut que pour une image : appelle ces fonctions à chaque image depuis un hook de rendu (PostDrawOpaqueRenderables ou PostDrawTranslucentRenderables en général ; aussi PreDrawHalos, RenderScreenspaceEffects, PostRender, ou EFFECT:Render). Ce qui est dessiné pendant une image est affiché à l'image suivante (un décalage d'environ 16 ms, invisible sauf sur un objet très rapide), puis oublié.

Tout est non éclairé, visible des deux côtés, avec la transparence de la couleur. Positions et tailles en mètres, dans le monde. Couleurs : Color(…) (blanc par défaut).

Matériau et mélange

Ces réglages s'appliquent au dessin qui suit ; ils reviennent à leur valeur par défaut au début de chaque image.

FonctionEffet
render.SetMaterial(mat)Image des formes suivantes (Material(…), un chemin, un matériau CreateMaterial ou une render target). Si le nom contient glow, light, flare ou sprites/, le dessin est additif (lumineux, le noir devient transparent) ; s'il contient ignorez, il est dessiné par-dessus le décor
render.SetColorMaterial()Plus d'image : couleur unie, cachée par le décor
render.SetColorMaterialIgnoreZ()Couleur unie, visible à travers les murs
render.OverrideBlend(actif, src, dst)actif = true avec dst = 1 (BLEND_ONE dans GMod ; ces constantes ne sont pas définies ici, écris le nombre) : additif ; sinon mélange normal (les autres arguments sont ignorés)
render.SetBlend(alpha) / render.GetBlend()Opacité multipliée (0 à 1)
render.SetColorModulation(r, g, b)Teinte multipliée, de 0 à 1 par composante
render.SetLightingMode, render.SuppressEngineLightingAcceptées, sans effet (le dessin n'est jamais éclairé)

Formes

FonctionForme
render.DrawBeam(a, b, largeur, t0, t1, couleur)Ruban plat de a à b, tourné vers la caméra. t0, t1 : coordonnée de l'image au début et à la fin (0 et 1 : l'image une fois sur la longueur)
render.StartBeam(n), render.AddBeam(pos, largeur, t, couleur), render.EndBeam()Faisceau en plusieurs segments (un éclair, une corde) : un point par AddBeam, dessiné par EndBeam. Les couleurs passent en dégradé d'un point à l'autre ; n est ignoré
render.DrawSprite(pos, largeur, hauteur, couleur)Rectangle centré sur pos, toujours face à la caméra
render.DrawQuad(a, b, c, d, couleur)Quadrilatère par ses quatre coins, dans l'ordre (image de (0, 0) en a à (1, 1) en c)
render.DrawQuadEasy(pos, normale, largeur, hauteur, couleur, rotation)Rectangle centré sur pos, tourné vers normale, tourné de rotation degrés autour d'elle
-- Un laser rouge depuis les yeux du joueur, avec une lueur au point d'impact.
local lueur = Material("physicore/particle_glow.png")

hook.Add("PostDrawTranslucentRenderables", "laser", function()
	local ply = LocalPlayer()
	if not ply:Alive() then return end
	local tr = ply:GetEyeTrace()
	local depart = ply:EyePos() + ply:GetRight() * 0.15 - Vector(0, 0.1, 0)
	render.SetColorMaterial()
	render.DrawBeam(depart, tr.HitPos, 0.02, 0, 1, Color(255, 30, 30, 200))
	render.SetMaterial(lueur)                  -- « glow » : additif
	render.DrawSprite(tr.HitPos, 0.4, 0.4, Color(255, 60, 60))
end)

Maillages : mesh et Mesh()

Pour des formes libres, l'API immédiate de GMod : mesh.Begin(type, nombre), puis pour chaque sommet mesh.Position(v), mesh.Color(r, g, b, a) (0 à 255), mesh.TexCoord(0, u, v), mesh.AdvanceVertex(), et enfin mesh.End(). Types : MATERIAL_TRIANGLES (3 sommets par triangle), MATERIAL_QUADS (4 par quadrilatère), MATERIAL_TRIANGLE_STRIP, MATERIAL_POLYGON (polygone convexe en éventail). nombre est ignoré, mesh.Normal et mesh.UserData ne font rien (pas d'éclairage). Aussi mesh.Quad(a, b, c, d, couleur) et mesh.QuadEasy(pos, normale, l, h, couleur).

Mesh() crée un maillage à construire une fois et à redessiner à chaque image : m:BuildFromTriangles({ {pos = Vector, u = 0, v = 0, color = Color}, … }) (3 sommets par triangle), m:Draw() (avec le matériau en cours), m:GetVertexCount(), m:Destroy(), m:IsValid(). Les positions sont dans le monde : il n'y a pas de matrice de modèle (cam.PushModelMatrix), recalcule les sommets pour déplacer la forme.

-- Un triangle aux couleurs dégradées au-dessus de l'origine de la map.
hook.Add("PostDrawOpaqueRenderables", "triangle", function()
	render.SetColorMaterial()
	mesh.Begin(MATERIAL_TRIANGLES, 1)
		mesh.Position(Vector(-1, 2, 0)) mesh.Color(255, 0, 0, 255) mesh.AdvanceVertex()
		mesh.Position(Vector(1, 2, 0))  mesh.Color(0, 255, 0, 255) mesh.AdvanceVertex()
		mesh.Position(Vector(0, 3.5, 0)) mesh.Color(0, 0, 255, 255) mesh.AdvanceVertex()
	mesh.End()
end)

Stencils

Le stencil limite le dessin à une zone dessinée avant : par exemple une « fenêtre » ou un portail. Il ne s'applique qu'au dessin 3D de cette section (formes, mesh, particules), pas aux modèles du jeu.

FonctionEffet
render.SetStencilEnable(bool)Active le stencil pour le dessin qui suit
render.SetStencilReferenceValue(n)Valeur de référence (0 à 255)
render.SetStencilCompareFunction(f)Test : STENCIL_NEVER, STENCIL_LESS, STENCIL_EQUAL, STENCIL_LESSEQUAL, STENCIL_GREATER, STENCIL_NOTEQUAL, STENCIL_GREATEREQUAL, STENCIL_ALWAYS (aussi nommées STENCILCOMPARISONFUNCTION_*)
render.SetStencilPassOperation(op)STENCIL_REPLACE : les pixels dessinés écrivent la valeur de référence. Les autres opérations (STENCIL_KEEP, ZERO, INCR…, aussi STENCILOPERATION_*) n'écrivent rien
render.SetStencilWriteMask, SetStencilTestMask, SetStencilFailOperation, SetStencilZFailOperation, ClearStencil, ClearStencilBufferRectangleAcceptées, sans effet
-- Un cadre écrit la valeur 1, puis la lueur n'apparaît que dans le cadre.
local lueur = Material("physicore/particle_glow.png")
hook.Add("PostDrawTranslucentRenderables", "fenetre", function()
	local centre = Vector(0, 2, -4)
	render.SetStencilEnable(true)
	render.SetStencilReferenceValue(1)
	render.SetStencilCompareFunction(STENCIL_ALWAYS)
	render.SetStencilPassOperation(STENCIL_REPLACE)
	render.SetColorMaterial()
	render.DrawQuadEasy(centre, Vector(0, 0, 1), 2, 2, Color(20, 20, 40))
	render.SetStencilCompareFunction(STENCIL_EQUAL)
	render.SetStencilPassOperation(STENCIL_KEEP)
	render.SetMaterial(lueur)
	render.DrawSprite(centre + Vector(math.sin(CurTime()) * 1.5, 0, 0.1), 1.5, 1.5, Color(255, 200, 80))
	render.SetStencilEnable(false)
end)
Limites du dessin 3D
  • 120 000 sommets au plus par image (un quadrilatère, un sprite ou un segment de faisceau en compte 6) ; au-delà, le dessin suivant est ignoré.
  • Affiché une image plus tard ; pas de matrices (cam.PushModelMatrix), pas d'éclairage, pas de lignes ni de points dans mesh.
  • Chaque changement de matériau, de mélange ou de stencil commence un nouveau groupe, dessiné après les précédents : regroupe les formes de même matériau.

Particules

effects.Particles(pos, options) → booléen

Une gerbe de particules éphémères (carrés toujours face à la caméra, non éclairés, qui s'estompent). Renvoie true si l'effet a été créé. Options, toutes facultatives :

OptionDéfautRôle
count20Nombre de particules (1 à 256)
colorblancColor
size0,1Taille en mètres (0,005 à 10)
speed3Vitesse max en m/s (la min est la moitié)
lifetime1Durée de vie en secondes (0,05 à 10)
gravity9,8Nombre en m/s² vers le bas (négatif = monte), ou un Vector
spread45Angle d'ouverture autour de la direction (0 à 180°)
directionvers le hautVector

64 gerbes vivantes et 2 000 particules au total au plus (au-delà, la gerbe est réduite ou refusée et la fonction renvoie false).

Émetteurs de particules : ParticleEmitter

Pour contrôler chaque particule, comme dans GMod : ParticleEmitter(pos, use3D) → émetteur, puis emetteur:Add(matériau, pos) → particule ou nil pour chaque particule. matériau : un Material(…) ou un chemin (par exemple les images intégrées "physicore/particle_glow.png", "physicore/particle_smoke.png", "physicore/particle_spark.png"). Une image dont le nom contient glow, light, flare ou fire est dessinée en additif (lumineuse). Les particules sont des carrés toujours face à la caméra, non éclairés, dessinés avec le dessin 3D de l'image.

Méthode de la particuleDéfautRôle
SetDieTime(s), SetLifeTime(s)1, 0Durée de vie, et âge de départ (secondes)
SetVelocity(v), SetGravity(v)immobile, aucuneVitesse (m/s) et accélération (Vector, m/s² ; Vector(0, -9.8, 0) = tombe)
SetAirResistance(n)0Freinage : la vitesse perd n % par seconde
SetStartSize(m), SetEndSize(m)0,1Largeur du carré en mètres, au début et à la fin (passage progressif)
SetStartAlpha(a), SetEndAlpha(a)255Opacité de 0 à 255, au début et à la fin
SetColor(r, g, b)255, 255, 255Teinte, de 0 à 255 (trois nombres, pas une Color)
SetRoll(degrés), SetRollDelta(degrés/s)0Rotation du carré et vitesse de rotation
SetCollide(bool), SetBounce(n)false, 0,3Rebondit sur le décor et les objets (trace à chaque image), en gardant n de sa vitesse
SetCollideCallback(fn)—fn(particule, pos, normale) à chaque rebond
SetThinkFunction(fn), SetNextThink(t)—fn(particule) est appelée à chaque image à partir de l'heure t (CurTime()) ; repousse SetNextThink dans la fonction pour l'espacer
SetPos(v), SetMaterial(mat)Déplacer, changer d'image

Chaque Set… a son Get… (GetColor() renvoie trois valeurs). SetAngles, SetAngleVelocity, SetLighting, SetStartLength / SetEndLength et SetVelocityScale gardent leur valeur mais ne changent pas l'affichage. Émetteur : SetPos(v) / GetPos() (position par défaut des particules ajoutées), GetNumActiveParticles(), SetNoDraw(bool), Is3D(), IsValid(), Finish() (l'émetteur disparaît quand ses dernières particules sont mortes ; appelle-le quand tu n'en ajoutes plus). 1 024 particules par émetteur et 4 096 en tout au plus : au-delà, Add renvoie nil.

-- Une fontaine d'étincelles pendant 3 secondes.
local etincelle = Material("physicore/particle_spark.png")

local function fontaine(pos)
	local em = ParticleEmitter(pos)
	timer.Create("fontaine", 0.05, 60, function()
		for i = 1, 4 do
			local p = em:Add(etincelle, pos)
			if p then
				p:SetVelocity(VectorRand() * 1.5 + Vector(0, 5, 0))
				p:SetGravity(Vector(0, -9.8, 0))
				p:SetDieTime(1.2)
				p:SetStartSize(0.08) p:SetEndSize(0)
				p:SetColor(255, 180, 60)
				p:SetCollide(true) p:SetBounce(0.4)
			end
		end
	end)
	timer.Simple(3.1, function() em:Finish() end)
end

util.Effect(nom, données) → booléen

Joue un effet, comme dans GMod, avec un objet EffectData() : data:SetOrigin(v), SetNormal(v), SetScale(n)… Le nom (majuscules indifférentes) désigne un effet Lua (EFFECT), sinon un effet intégré : "Sparks" (étincelles), "BloodImpact", "Explosion", "WaterSplash", "Dust", "Smoke". Un effet intégré utilise Origin (position), Normal (direction), Scale (0,1 à 10, multiplie taille et vitesse) et Color ; un nom inconnu affiche une erreur et renvoie false.

Champs de EffectData() (chacun avec Set… et Get…, nil s'il n'a pas été donné) : Origin, Start, Normal, Angles, Magnitude, Scale, Radius, Entity, Flags, Color, SurfaceProp, HitBox, AttachmentNumber, DamageType, MaterialIndex, EntIndex. Ils contiennent ce que tu veux (nombre, Vector, Color, entité…).

Côté serveur, util.Effect(nom, données) joue l'effet chez tous les joueurs (chacun le joue avec son Lua client ; l'entité arrive comme une entité de chez lui) et renvoie true. Côté client, il ne le joue que pour ce joueur.

Effets en Lua : EFFECT

Un effet Lua est une table EFFECT, côté client seulement, déclarée par un fichier :

OùFichier
Gamemodeeffects/<nom>.lua
Addonlua/effects/<nom>.lua
N'importe quel fichier clienteffects.Register(EFFECT, "nom")

Pendant le chargement du fichier, la table globale EFFECT est prête ; le nom de l'effet est le nom du fichier sans .lua, en minuscules. Aussi effects.Create(nom) (la table EFFECT ou nil) et effects.GetList() (les noms). Chaque util.Effect crée un objet qui hérite de la table :

FonctionQuand
EFFECT:Init(data)À la création, avec le EffectData() reçu
EFFECT:Think()Chaque image. Renvoyer false supprime l'effet ; nil ou true le garde
EFFECT:Render()Chaque image après Think : dessine avec render.DrawSprite, DrawBeam, mesh…
Fin de l'effetUn effet ne disparaît que si Think renvoie false (ou si Think ou Render provoque une erreur). Sans Think, il reste jusqu'au rechargement des scripts. 256 effets actifs au plus : au-delà, util.Effect renvoie false.
-- lua/effects/onde_choc.lua (addon) : un anneau lumineux qui s'élargit pendant 0,6 s.
local lueur = Material("physicore/particle_glow.png")

function EFFECT:Init(data)
	self.Pos = data:GetOrigin() or Vector(0, 0, 0)
	self.Taille = data:GetScale() or 1
	self.Fin = CurTime() + 0.6
end

function EFFECT:Think()
	return CurTime() < self.Fin          -- false à la fin : l'effet est supprimé
end

function EFFECT:Render()
	local f = 1 - (self.Fin - CurTime()) / 0.6
	render.SetMaterial(lueur)
	render.DrawQuadEasy(self.Pos + Vector(0, 0.05, 0), Vector(0, 1, 0), 6 * f * self.Taille, 6 * f * self.Taille,
		Color(120, 200, 255, 255 * (1 - f)))
end
-- Serveur : une onde de choc chez tous les joueurs à l'atterrissage d'un joueur.
hook.Add("OnPlayerHitGround", "onde", function(ply, eau, flotteur, vitesse)
	if vitesse < 8 then return end
	local data = EffectData()
	data:SetOrigin(ply:GetPos())
	data:SetScale(vitesse / 10)
	util.Effect("onde_choc", data)
end)

Décalcomanies : util.Decal

util.Decal(image, pos, normale, taille, durée, couleur) → booléen projette une image sur les surfaces autour de pos (y compris les props et joueurs qui passent dedans).

Forme de GMod : util.Decal(nom, début, fin)

La forme de Garry's Mod marche aussi : util.Decal(nom, début, fin) trace un rayon de début à fin et pose la décalcomanie là où il touche (rien s'il ne touche rien ; le 4e argument, le filtre de GMod, est ignoré). Le jeu reconnaît cette forme quand le 3e argument est un point (pas une direction de longueur 1). Noms de GMod (majuscules indifférentes) : Blood, RedGlowFade, YellowBlood, AlienBlood, Antlion.Splat, Scorch, FadingScorch, SmallScorch, Explosion, BeerSplash, PaintSplatBlue, PaintSplatGreen, PaintSplatPink, PaintSplatOrange, et tous les Impact.* (petite trace d'impact) ; ils marchent aussi dans la forme du jeu.

Côté serveur, util.Decal(nom, début, fin) (cette forme seulement) pose la décalcomanie chez tous les joueurs.

-- Serveur : une tache de sang sous un joueur blessé.
hook.Add("PlayerHurt", "taches", function(ply)
	local pos = ply:GetPos() + Vector(0, 0.5, 0)
	util.Decal("Blood", pos, pos - Vector(0, 2, 0))
end)
-- Impact de balle côté client (reçu par un message net).
net.Receive("impact", function()
	local pos = net.ReadVector()
	local normale = net.ReadVector()
	local data = EffectData()
	data:SetOrigin(pos)
	data:SetNormal(normale)
	data:SetScale(0.6)
	util.Effect("Sparks", data)
	util.Decal("scorch", pos, normale, 0.15, 20)
end)

Effets d'écran

Les fonctions render.* ci-dessous restent actives jusqu'à ce qu'on les change ; pour les fonctions de GMod appelées à chaque image (DrawColorModify, DrawBloom…), voir plus bas. Ils passent sous le HUD (le HUD et les fenêtres Lua restent nets) et sont retirés quand les scripts sont rechargés.

FonctionEffet
render.ScreenFade(couleur, durée, maintien, entrant) (alias render.SetScreenFade)entrant faux (défaut) : l'écran est de la couleur pendant maintien secondes (0 par défaut) puis revient à la normale en durée secondes (flash, éblouissement). entrant = true : l'écran passe à la couleur en durée secondes et y reste (fondu au noir) jusqu'à ClearScreenFade ou un nouveau fondu. L'alpha de la couleur est l'opacité maximale ; durées de 60 s au plus.
render.ClearScreenFade()Retire le fondu
render.SetColorModify(valeurs){ brightness = 0 (−1 à 1, ajouté), contrast = 1 (0 à 5), saturation = 1 (0 à 5 ; 0 = noir et blanc), tint = Color (multiplie, blanc par défaut) }. Les clés GMod "$pp_colour_brightness", "$pp_colour_contrast", "$pp_colour_colour" sont acceptées. Sans argument (ou valeurs neutres) : retour à la normale.
render.SetBlur(force)Flou de l'écran, de 0 (aucun) à 10
-- Gris et flou quand la santé est basse, flash rouge quand on est touché.
local derniere = 100
hook.Add("Think", "EffetsSante", function()
	local hp = LocalPlayer():Health()
	if hp < derniere then
		render.ScreenFade(Color(200, 0, 0, 120), 0.4)
	end
	derniere = hp
	if hp < 30 and LocalPlayer():Alive() then
		render.SetColorModify({ saturation = 0.3, contrast = 1.1 })
		render.SetBlur(1.5)
	else
		render.SetColorModify()
		render.SetBlur(0)
	end
end)

Effets d'écran de l'image : RenderScreenspaceEffects

Les fonctions de post-traitement de GMod, à appeler à chaque image dans le hook RenderScreenspaceEffects : elles sont remises à zéro avant le hook, et un effet qui n'est plus appelé disparaît à l'image suivante. Elles passent aussi sous le HUD.

FonctionEffet
DrawColorModify(t)Clés GMod : ["$pp_colour_brightness"] (ajoutée), ["$pp_colour_contrast"] (1 = normal), ["$pp_colour_colour"] (saturation, 0 = noir et blanc), ["$pp_colour_addr"], addg, addb (ajoutés au rouge, vert, bleu), ["$pp_colour_mulr"], mulg, mulb (multiplient la composante par 1 + valeur). Valeurs de −10 à 10. Donne à chaque appel toutes les clés que tu utilises
DrawBloom(seuil, force, tailleX, tailleY, passes, mulCouleur, r, g, b)Halo autour des zones claires : ce qui dépasse seuil (0 à 1, 0,65 par défaut) est flouté (tailleX), multiplié par force et par la couleur r, g, b (de 0 à 1) × mulCouleur, puis ajouté. tailleY et passes sont ignorés
DrawSharpen(contraste, distance)Netteté (0 à 10 ; distance en pixels)
DrawSobel(seuil)Contours noirs sur fond blanc (seuil de 0,01 à 1 : plus petit = plus de traits)
DrawToyTown(passes, hauteur)Effet maquette : flou en haut et en bas de l'écran. Force = passes / 4 ; hauteur en pixels (comme GMod) ou en fraction de l'écran si ≤ 1
DrawMotionBlur(ajout, opacité, délai)Approché : un simple flou de force opacité × (1 − ajout) (0,9 au plus), sans traînée
DrawTexturize(échelle, mat)Approché : l'image mat mélangée à 50 % sur l'écran
DrawMaterialOverlay(mat, réfraction)L'image mat étirée sur tout l'écran, avec sa transparence (lunettes, masque, vitre sale) ; réfraction est ignorée
-- Vision nocturne : vert, lumineux, avec un halo sur les lumières.
local actif = false
concommand.Add("vision_nuit", function() actif = not actif end)

hook.Add("RenderScreenspaceEffects", "vision_nuit", function()
	if not actif then return end
	DrawColorModify({
		["$pp_colour_brightness"] = 0.1, ["$pp_colour_contrast"] = 1.4, ["$pp_colour_colour"] = 0,
		["$pp_colour_addr"] = 0, ["$pp_colour_addg"] = 0.15, ["$pp_colour_addb"] = 0,
		["$pp_colour_mulr"] = 0, ["$pp_colour_mulg"] = 0, ["$pp_colour_mulb"] = 0,
	})
	DrawBloom(0.6, 1.5, 6, 6, 1, 1, 0.4, 1, 0.4)
end)

Lumières dynamiques et projecteurs

DynamicLight(id)

Une lumière ponctuelle temporaire (tir, explosion, torche), côté client. DynamicLight(id) renvoie la table de la lumière id (un nombre ou un texte ; la crée au premier appel, sinon renvoie la même) ; on règle ses champs, et le jeu la place à chaque image tant que dietime n'est pas passé :

ChampDéfautRôle
posVector(0, 0, 0)Position dans le monde
r, g, b255Couleur, de 0 à 255
brightness1Intensité (0 à 32)
size4Portée en mètres (0,1 à 200)
dietimeCurTime() + 1 à la créationHeure (CurTime()) où elle s'éteint et est oubliée

Les noms en majuscules de GMod (Pos, Brightness, Size, DieTime) sont lus aussi ; decay est accepté mais ignoré (la lumière ne faiblit pas). 32 lumières allumées au plus, sans ombres. Pour une lumière fixée à une pièce d'un modèle (phares, gyrophares), voir ents.CreateClientLight.

-- Client : éclair orange de 0,1 s à chaque tir du joueur local.
hook.Add("EntityFireBullets", "eclair_tir", function(tireur, balle)
	if CLIENT and tireur == LocalPlayer() then
		local l = DynamicLight("tir")          -- toujours la même lumière, ravivée à chaque tir
		l.pos = tireur:EyePos() + tireur:GetAimVector() * 1
		l.r, l.g, l.b = 255, 170, 60
		l.brightness = 3
		l.size = 6
		l.dietime = CurTime() + 0.1
	end
end)

ProjectedTexture()

Un projecteur (lampe torche, projecteur de diapositives) : une lumière en cône qui peut projeter une image. ProjectedTexture() renvoie l'objet (16 au plus ; au-delà, une erreur et nil). Il reste invisible tant que Update() n'a pas été appelé, et les changements de position et d'angles ne s'appliquent qu'à l'appel suivant de Update().

MéthodeRôle
SetPos(v) / GetPos(), SetAngles(ang) / GetAngles()Position et orientation ; la lumière part vers l'avant des angles (comme ang:Forward())
SetTexture(mat)Image projetée (Material(…), chemin, render target)
SetFOV(degrés)Ouverture du cône (2 à 178°) ; SetHorizontalFOV et SetVerticalFOV font la même chose
SetFarZ(m)Portée (0,1 à 500 m, 50 par défaut) ; SetNearZ est sans effet
SetColor(Color), SetBrightness(n)Couleur, intensité (0 à 64)
SetEnableShadows(bool)Ombres (plus coûteux)
Update(), Remove(), IsValid()Applique et montre, supprime, existe encore ?
-- Une lampe frontale qui suit la vue du joueur local.
local lampe = ProjectedTexture()
lampe:SetFOV(50)
lampe:SetFarZ(30)
lampe:SetBrightness(4)
lampe:SetColor(Color(255, 240, 220))
hook.Add("Think", "lampe", function()
	if not IsValid(lampe) then return end
	local ply = LocalPlayer()
	lampe:SetPos(ply:EyePos())
	lampe:SetAngles(ply:EyeAngles())
	lampe:Update()
end)

Modèles côté client : ClientsideModel

ClientsideModel(id) → modèle ou nil affiche un modèle (id comme pour ents.SpawnProp : "builtin:crate_small", "addon:<clé>/models/x.glb", "gadget:…") que seul ce joueur voit : pas de collision, pas de physique, pas de réseau. Il apparaît à l'origine (0, 0, 0). 128 au plus ; tous sont supprimés quand les scripts sont rechargés. Renvoie nil si l'id est inconnu.

MéthodeRôle
:SetPos(v) / :GetPos()Position dans le monde
:SetAngles(ang) / :GetAngles()Orientation dans le monde (degrés)
:SetLocalPos(v) / :SetLocalAngles(ang)Position / orientation par rapport au parent
:SetModelScale(n) / :GetModelScale()Échelle (0,01 à 100)
:SetColor(Color) / :GetColor()Teinte ; alpha < 255 = transparent
:SetNoDraw(bool) / :GetNoDraw()Cacher / montrer
:SetParent(ent)Suit un prop, un joueur (son corps, qui tourne avec lui) ou un autre modèle client, en gardant sa position actuelle ; nil le détache. Si le parent disparaît, le modèle disparaît aussi (IsValid devient faux).
:Remove(), :IsValid(), :GetModel()Supprimer, existe encore ?, id du modèle

Un modèle client marche avec halo.Add et avec IsValid(modele). Pour un aperçu dans une fenêtre, voir aussi DModelPanel.

-- Chapeau sur la tête du joueur local, visible seulement par lui (aperçu de boutique).
local chapeau
function ApercuChapeau() -- appelée par exemple par un bouton d'une fenêtre vgui
	if IsValid(chapeau) then chapeau:Remove() end
	chapeau = ClientsideModel("builtin:cylinder")
	if not chapeau then return end
	chapeau:SetModelScale(0.5)
	chapeau:SetColor(Color(40, 40, 40))
	chapeau:SetPos(LocalPlayer():GetPos() + Vector(0, 1.95, 0))
	chapeau:SetParent(LocalPlayer())
end

Matériaux créés en Lua : CreateMaterial

Côté client, CreateMaterial(nom, shader, paramètres) crée un matériau (256 au plus) et renvoie son objet ; Material(nom) le retrouve ensuite. Le shader "UnlitGeneric" est dessiné sans lumière, les autres ("VertexLitGeneric"…) avec. Paramètres reconnus :

ParamètreEffet
["$basetexture"]Image : chemin comme pour Material, nom d'un autre matériau ou d'une render target (ou l'objet lui-même)
["$color"] (ou $color2)Teinte : Vector(r, g, b) de 0 à 1, Color, ou texte "[1 0.5 0]" / "{255 128 0}"
["$alpha"]Opacité de 0 à 1
["$translucent"], ["$vertexalpha"]Transparence de l'image
["$additive"]Dessin additif (lumineux)
["$nocull"]Visible des deux côtés

Méthodes : mat:SetTexture(param, valeur), SetVector, SetFloat, SetInt, SetString (changent un paramètre et refont le matériau), GetTexture(param), GetVector, GetFloat, GetInt, GetString, GetName(), GetShader(), Width(), Height(), IsError(), Recompute(). Il s'utilise avec surface.SetMaterial, et sur un prop : côté serveur, ent:SetMaterial("!nom") envoie le nom à tous, et chaque joueur y met son propre matériau de ce nom (créé par le Lua client ; tant qu'il n'existe pas, le prop garde son matériau d'origine).

Render targets : caméras et écrans

Côté client, GetRenderTarget(nom, largeur, hauteur) (aussi GetRenderTargetEx, mêmes trois premiers arguments) crée une texture dans laquelle une caméra dessine la scène (16 au plus, de 16 à 2048 px ; le même nom redonne la même). Pour la remplir : render.PushRenderTarget(rt), render.RenderView({ origin, angles, fov, znear, zfar }), render.PopRenderTarget(). L'image est rendue à la fin de l'image en cours, une fois par appel : rappelle RenderView (par exemple dans Think) pour une image animée. RenderView ne dessine que dans une render target poussée (pas directement à l'écran). Aussi render.SetRenderTarget(rt), render.GetRenderTarget() ; render.Clear(…) existe et ne fait rien (la caméra redessine toute la texture). Objet : GetName(), Width(), Height(), IsError().

-- Client : un écran de surveillance sur un prop (le serveur a fait ecran:SetMaterial("!camera_ecran")).
local rt = GetRenderTarget("camera_rt", 512, 288)
CreateMaterial("camera_ecran", "UnlitGeneric", { ["$basetexture"] = rt })

hook.Add("Think", "camera", function()
	render.PushRenderTarget(rt)
	render.RenderView({ origin = Vector(0, 6, -10), angles = Angle(-20, 0, 0), fov = 70 })
	render.PopRenderTarget()
end)

Limites