```lua
-- cl_realistic_smoke.lua

if CLIENT then
    -- Таблица для хранения всех активных частиц дыма
    local SmokeParticles = {}

    -- Настройки дыма
    local SmokeSettings = {
        EmitRate = 10,            -- Количество частиц, создаваемых за кадр
        MaxParticles = 500,       -- Максимальное количество частиц
        SpawnPosition = Vector(0, 0, 0), -- Позиция эмиттера (можно изменить динамически)
        Spread = 50,              -- Разброс начальной позиции частиц
        InitialVelocity = Vector(0, 0, 50), -- Начальная скорость частиц
        VelocityRandomness = 20,  -- Разброс скорости
        ParticleLifetime = {2, 5},-- Диапазон времени жизни частиц (секунды)
        ParticleSize = {10, 30},  -- Диапазон начального размера частиц
        FadeOutTime = 1,          -- Время плавного исчезновения
        Color = Color(150, 150, 150), -- Цвет дыма
        Texture = "particle/particle_smokegrenade", -- Текстура частицы
    }

    -- Класс SmokeParticle
    local SmokeParticle = {}
    SmokeParticle.__index = SmokeParticle

    function SmokeParticle:New(pos)
        local self = setmetatable({}, SmokeParticle)
        -- Начальная позиция с разбросом
        self.pos = pos + Vector(math.Rand(-SmokeSettings.Spread, SmokeSettings.Spread),
                               math.Rand(-SmokeSettings.Spread, SmokeSettings.Spread),
                               math.Rand(0, 10))
        -- Начальная скорость с разбросом
        self.velocity = SmokeSettings.InitialVelocity + Vector(math.Rand(-SmokeSettings.VelocityRandomness, SmokeSettings.VelocityRandomness),
                                                              math.Rand(-SmokeSettings.VelocityRandomness, SmokeSettings.VelocityRandomness),
                                                              math.Rand(-10, 10))
        self.size = math.Rand(SmokeSettings.ParticleSize[1], SmokeSettings.ParticleSize[2])
        self.maxSize = self.size * 3
        self.color = SmokeSettings.Color
        self.alpha = 255
        self.spawnTime = CurTime()
        self.lifetime = math.Rand(SmokeSettings.ParticleLifetime[1], SmokeSettings.ParticleLifetime[2])
        self.texture = SmokeSettings.Texture
        self.collided = false
        return self
    end

    -- Обновление частицы
    function SmokeParticle:Update(dt)
        if self.collided then
            -- Если частица столкнулась, можно добавить эффект дыма, задержку и т.д.
            return
        end

        -- Обновление позиции
        self.pos = self.pos + self.velocity * dt

        -- Гравитация (опционально)
        self.velocity = self.velocity + Vector(0, 0, -10) * dt

        -- Проверка коллизий с миром
        local tr = util.TraceLine({
            start = self.pos - self.velocity * dt, -- Предыдущая позиция
            endpos = self.pos,
            mask = MASK_SOLID_BRUSHONLY -- Проверка только на статические объекты
        })

        if tr.Hit then
            self.pos = tr.HitPos
            self.velocity = Vector(0, 0, 0)
            self.collided = true
        end

        -- Увеличение размера для эффекта расширения дыма
        self.size = math.min(self.size + 10 * dt, self.maxSize)

        -- Плавное уменьшение альфа для исчезновения
        local age = CurTime() - self.spawnTime
        if age > (self.lifetime - SmokeSettings.FadeOutTime) then
            local fadeAlpha = 255 * (1 - (age - (self.lifetime - SmokeSettings.FadeOutTime)) / SmokeSettings.FadeOutTime)
            self.alpha = math.Clamp(fadeAlpha, 0, 255)
        end
    end

    -- Отрисовка частицы
    function SmokeParticle:Draw()
        if self.alpha <= 0 then return end

        render.SetMaterial(Material(self.texture))
        local color = Color(self.color.r, self.color.g, self.color.b, self.alpha)
        render.DrawSprite(self.pos, self.size, self.size, color)
    end

    -- Функция для эмитирования новых частиц
    local function EmitSmokeParticles()
        -- Определение позиции эмиттера (можно заменить на динамическую, например, позицию игрока)
        local emitterPos = SmokeSettings.SpawnPosition

        for i = 1, SmokeSettings.EmitRate do
            if #SmokeParticles >= SmokeSettings.MaxParticles then break end
            table.insert(SmokeParticles, SmokeParticle:New(emitterPos))
        end
    end

    -- Функция для обновления и отрисовки частиц
    local function UpdateAndDrawParticles()
        local dt = FrameTime()
        for i = #SmokeParticles, 1, -1 do
            local particle = SmokeParticles[i]
            local age = CurTime() - particle.spawnTime

            if age > particle.lifetime then
                table.remove(SmokeParticles, i)
            else
                particle:Update(dt)
                particle:Draw()
            end
        end
    end

    -- Добавляем хуки
    hook.Add("Think", "RealisticSmokeThink", function()
        EmitSmokeParticles()
    end)

    hook.Add("PostDrawTranslucentRenderables", "RealisticSmokeDraw", function()
        UpdateAndDrawParticles()
    end)

    -- Опционально: Обработка команды для изменения позиции эмиттера
    concommand.Add("set_smoke_position", function(ply, cmd, args)
        if args[1] and args[2] and args[3] then
            local x = tonumber(args[1]) or 0
            local y = tonumber(args[2]) or 0
            local z = tonumber(args[3]) or 0
            SmokeSettings.SpawnPosition = Vector(x, y, z)
            print("Smoke emitter position set to:", SmokeSettings.SpawnPosition)
        else
            print("Usage: set_smoke_position <x> <y> <z>")
        end
    end)

    -- Инициализация: Установим позицию эмиттера на позицию игрока при подключении
    hook.Add("InitPostEntity", "InitializeSmokeEmitter", function()
        local ply = LocalPlayer()
        if ply:IsValid() then
            SmokeSettings.SpawnPosition = ply:GetPos() + Vector(0, 0, 50) -- Например, над головой игрока
        end
    end)
end
```
Сделай реалистичный дым, по реальным формулам