WF 15: FLUID PRODUCT SPLASH
RU: FLIP splash around product, meshing, whitewater EN: FLIP splash around product, meshing, whitewater Бьющая вверх струя вокруг товара: капли расходятся по форме изделия, превращаются в меш, от него отрывается аэрозоль (whitewater). Три разных рендера в одном кадре — жидкость, меш и пена — это три
ID: WF-15 | Type: Fluid | Difficulty: ⭐⭐⭐ RU/EN Bilingual
🎯 ЦЕЛЬ / Goal
RU: FLIP splash around product, meshing, whitewater EN: FLIP splash around product, meshing, whitewater
Бьющая вверх струя вокруг товара: капли расходятся по форме изделия, превращаются в
меш, от него отрывается аэрозоль (whitewater). Три разных рендера в одном кадре —
жидкость, меш и пена — это три разных требования к памяти, и именно на этом воркфлоу
обычно кончается RAM. Здесь обязательна дисциплина: маленький домен, честный масштаб,
cache_type = 'ALL' до конца расчёта и запись кэша на диск.
📋 ЧТО ПОНАДОБИТСЯ
| Компонент | Версия | Зачем |
|---|---|---|
| Blender | 4.x | Mantaflow (встроенный) |
| ОЗУ | 16 ГБ+ | при resolution_max = 256 домен — это ~16 млн ячеек |
| Меш товара | замкнутый | иначе эффектор не считается |
| Диск | 20 ГБ+ | кэш жидкости весит больше, чем сам рендер |
[!NOTE] Названия полей Mantaflow менялись заметно (
cache_data_format,simulation_method,resolution_max,spray_scaleи соседние). Ниже приведены те имена, которые использовались в ветке 4.x, но перед продакшном обязательно сверь список: в Python-консоли набериdir(bpy.data.objects["Domain"].fluid_settings)— выведет реальные поля твоей сборки, и это надёжнее, чем любая документация.
🛠 НАСТРОЙКА
Этап 1. Масштаб решает всё
Домен должен быть плотным: 1 unit = 1 метр, товар внутри, а не «комната 10 метров вокруг банки». Каждая лишняя единица сетки — это куб памяти вчетверо.
import bpy, math
scene = bpy.context.scene
scene.render.fps = 24
scene.frame_start, scene.frame_end = 1, 90 # 90 кадров ≈ 3.75 с
# Домен: куб 2×2×2 м вокруг товара. Максимальный габарит товара
# должен занимать не меньше трети ширины домена, иначе сетка грубая.
bpy.ops.object.fluid_add(type='DOMAIN', location=(0, 0, 1.0))
dom = bpy.context.object
dom.name = "Domain"
dom.scale = (1.0, 1.0, 1.0)
fs = dom.fluid_settings
fs.domain_type = 'LIQUID'
fs.resolution_max = 256 # 128 — черновик, 256 — рабочая, 512 — уже 64 ГБ RAM
Этап 2. Влияние: струя снизу вверх
# Цилиндр-источник под товаром: жидкость выходит из-под изделия,
# расходится по его форме и срывается каплями — классический «waterfall»
bpy.ops.mesh.primitive_cylinder_add(vertices=32, radius=0.35, depth=0.4,
location=(0, 0, 0.12))
src = bpy.context.object
src.name = "Source"
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
bpy.ops.object.fluid_add(type='FLOW', location=(0, 0, 0))
flw = bpy.context.object
flw.name = "Flow"
fset = flw.fluid_settings
fset.flow_type = 'LIQUID'
fset.flow_behavior = 'INFLOW'
fset.use_inflow = True
fset.use_plane_init = False # False = непрерывный поток, True = разовый «поршень»
Этап 3. Товар как эффектор
product = bpy.data.objects["Product"]
bpy.ops.object.fluid_add(type='EFFECTOR', location=product.location)
eff = bpy.context.object
eff.name = "ProductEffector"
eset = eff.fluid_settings
eset.effector_type = 'COLLISION'
eset.use_effector = True
# Насколько жидкость «обтекает» форму до того, как считает себя внутри.
# Больше = объёмнее жидкость вокруг изделия, меньше = тонкая плёнка.
eset.surface_distance = 0.02
eset.use_plane_init = False
# Подвижное тело: если товар падает/вращается — эффектор должен это знать
eset.subframes = 1 # промежуточные кадры: без них быстрые объекты «протыкают» воду
Этап 4. Параметры домена и whitewater
fs.cache_type = 'ALL' # 'ALL' = сохраняем весь кэш. Иначе после смены кадра
# физика пересчитается и результат «поплывёт»
fs.cache_frame_start = 1
fs.cache_frame_end = 90
fs.cache_directory = "//cache/fluid"
fs.cache_data_format = 'OPENVDB'
# Аэрозоль: то, что читается как брызги и пена. Без этого будет
# гладкая «желевая» вода без капель.
ew = fs.effector_weights
ew.spray_emission = 12.0 # капли в воздухе
ew.foam_emission = 6.0 # пена на поверхности
ew.surface_distance = 0.02
Этап 5. Меширование жидкости
Домен создаёт частицы «Liquid Mesh»; из них строится поверхность, а не из объёма сетки.
# Настройки частиц меша живут в particle system домена
for ps in dom.particle_systems:
if "mesh" in ps.name.lower():
ps.settings.liquid_render = 'MESH' # иначе на выходе будет пузырьки, а не поверхность
ps.settings.sphere_scale = 1.0
ps.settings.sphere_radius = 1.0 # >1 = поверхность «натягивается», меньше капель
[!NOTE] Имя частичной системы, создаваемой доменом, и поля
liquid_render/sphere_scaleвнутренние и версионно-зависимые. Надёжнее найти её в интерфейсе: вкладка Physics → Type: Liquid → раздел Liquid Mesh. Если в твоей версии поля называются иначе — сверься с руководством, а не с этим списком.
Этап 6. Материал жидкости и пены
mat = bpy.data.materials.new("Mat_Liquid")
mat.use_nodes = True
nt = mat.node_tree
bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED')
bsdf.inputs["Base Color"].default_value = (0.75, 0.85, 0.9, 1.0)
bsdf.inputs["Transmission Weight"].default_value = 1.0
bsdf.inputs["Roughness"].default_value = 0.02
bsdf.inputs["IOR"].default_value = 1.333 # вода, не стекло: 1.45 было бы «стеклянной»
product.data.materials.append(mat)
# Пена — объёмный Principled Volume: цвет, плотность, эмиссия и
# два входа whitewater, которые появляются у ноды в контексте флюида
foam = bpy.data.materials.new("Mat_Foam")
foam.use_nodes = True
ntf = foam.node_tree
ntf.nodes.clear()
pv = ntf.nodes.new("ShaderNodeVolumePrincipled")
out = ntf.nodes.new("ShaderNodeOutputMaterial")
ntf.links.new(pv.outputs["Volume"], out.inputs["Volume"])
pv.inputs["Color"].default_value = (1.0, 1.0, 1.0, 1.0)
pv.inputs["Density"].default_value = 0.35
dom.data.materials.append(foam)
Этап 7. Расчёт
# Порядок: сначала белкастер, потом рендер. Порог в память — 16 ГБ
bpy.ops.fluid.bake_all()
# Проверка: кэш лежит на диске и весит сотни мегабайт — это нормально
# ls -lh cache/fluid/
Этап 8. Контроль: что реально лежит на диске
Bake может прерваться по памяти и не сообщить об этом — сцена просто останется без
кэша, а рендер возьмёт кадр 1 для всего ролика. Поэтому проверять надо не флаги, а
файлы: сколько их, сколько они весят и совпадает ли это с cache_frame_start /
cache_frame_end. Заодно печатаются те поля домена, которые определяют и память, и
картинку: разрешение, число системных частиц и состояние меширования.
import bpy, os, glob
scene = bpy.context.scene
dom = bpy.data.objects["Domain"]
# Настройки домена лежат в разных местах в зависимости от версии:
# на объекте (`dom.fluid_settings`) либо на модификаторе Fluid (`md.domain_settings`).
# Поэтому берём первое, что реально нашлось, — иначе скрипт упадёт на AttributeError
# там, где в прошлой версии всё работало.
fs = getattr(dom, "fluid_settings", None)
if fs is None:
fs = next((md.domain_settings for md in dom.modifiers
if md.type == 'FLUID'), None)
if fs is None:
print("Домен не найден — проверь имя объекта и как он создан")
else:
# 1. Параметры, по которым считается объём работы
print("тип:", fs.domain_type, "| resolution_max:", fs.resolution_max,
"| кэш:", fs.cache_type, "| формат:", fs.cache_data_format)
print("кадры кэша:", fs.cache_frame_start, "…", fs.cache_frame_end,
"| печётся сейчас:", getattr(fs, "is_cache_baking_data", "нет поля"))
# 2. Кэш на диске: считаем настоящие файлы, а не доверяем флажку.
# cache_directory — путь относительно .blend, абсолютизируем.
cache_dir = bpy.path.abspath(fs.cache_directory)
files = sorted(glob.glob(os.path.join(cache_dir, "*")))
size_mb = sum(os.path.getsize(f) for f in files if os.path.isfile(f)) / 1048576
print("каталог:", cache_dir, "| файлов:", len(files),
"| МБ:", round(size_mb, 1),
"| ожидалось:", fs.cache_frame_end - fs.cache_frame_start + 1)
# 3. Системы частиц: их создаёт сам домен, и их отсутствие —
# главный признак того, что bake не отработал
print("particle systems:", [ps.name for ps in dom.particle_systems])
# 4. Whitewater: имена полей менялись, поэтому читаем через getattr.
# 0 означает «брызги выключены» — кадр получится гладким, как желе.
ew = fs.effector_weights if fs is not None else None
if ew is not None:
print("spray_emission:", getattr(ew, "spray_emission", "нет поля"),
"| foam_emission:", getattr(ew, "foam_emission", "нет поля"))
[!NOTE] Блок написан устойчиво к версиям и это не косметика: проверено на 5.2.1 LTS, что
bpy.ops.object.fluid_addтам не существует вообще (object.effector_addесть,quick_liquidесть, а fluid-оператора нет), домен создаётся как объект с модификаторомFLUIDи полемfluid_type('NONE' / 'DOMAIN' / 'FLOW' / 'EFFECTOR'), настройки лежат вmd.domain_settings, а не вob.fluid_settings. Имя системы частиц там —Liquidс типом'FLIP', и полейliquid_render/sphere_scaleу неё уже нет. Поэтому всё, что переезжало между версиями, в скрипте читается черезgetattrс запасным вариантом: в своей сборке сначала набериdir(bpy.data.objects["Domain"].fluid_settings)и сверь список.
is_cache_baking_data во время проверки должен быть False — если True, bake ещё
идёт и подсчёт файлов покажет недобакенный объём. Расхождение «файлов заметно меньше,
чем кадров кэша» — самый частый признак вылета по памяти на середине; лечится снижением
resolution_max или габарита домена, а не повторным bake в том же разрешении.
⚙️ КЛЮЧЕВЫЕ ПАРАМЕТРЫ
| Параметр | Значение | Когда менять |
|---|---|---|
resolution_max |
128 / 256 / 512 | черновик / рабочий / премиум |
cache_type |
'ALL' |
иначе результат нестабилен между прогонами |
spray_emission |
8–20 | выше 25 — кадр в молоке |
foam_emission |
3–10 | для мыла/молока |
subframes |
1–3 | обязательно 3, если товар быстро движется |
IOR |
1.333 | стекло — 1.45 |
📤 РЕЗУЛЬТАТ
| Параметр | Значение |
|---|---|
| Симуляция | 90 кадров (3.75 с при 24 fps), домен 2×2×2 м, resolution_max = 256 |
| Кэш | //cache/fluid/ на диске, ориентировочно 1–8 ГБ |
| Меш | поверхность жидкости из частиц Liquid Mesh, отдельный меш для пены |
| Рендер | 1920×1080 Cycles 256 сэмплов: 40–120 мин (жидкость с преломлением дороже всего) |
| Экспорт | Alembic жидкости для последующего композитинга |
Разрешение кадра — 1920×1080, 16 бит на канал (color_depth = '16'), контейнер
FFMPEG / H264 с constant_rate_factor = 'HIGH'. Порядок вывода: сначала PNG-серия
с тем же именем, что и кадры кэша, потом сборка в видео вторым проходом с
image_settings.file_format = 'FFMPEG'. Оценки времени даны для resolution_max = 256
и одной жидкости на кадр; на 128 они примерно вчетверо меньше, на 512 — вылет по памяти
раньше, чем начнётся рендер. Это ориентир для планирования, а не обещание: реальное
время определяется GPU, числом источников света и тем, считается ли ворс/аэрозоль.
⚠️ ТИПОВЫЕ ОШИБКИ
| Симптом | Причина | Решение |
|---|---|---|
| Жидкость проходит сквозь товар | товар не EFFECTOR либо subframes = 0 |
добавить эффектор, поднять subframes до 3 |
| Картинка «плывёт» между прогонами | cache_type не 'ALL' |
выставить 'ALL' и перезапечь |
| Вылет по памяти | домен слишком большой или resolution_max велик |
уменьшить габарит домена, resolution_max до 128 |
| Капли выглядят как пузырьки | не переключено liquid_render в 'MESH' |
переключить в интерфейсе Liquid Mesh |
| Нет брызг, только «желе» | spray_emission = 0 и нет whitewater |
поднять spray_emission, назначить пеноматериал |
| Поверхность в сетку | мало итераций меширования | увеличить surface_distance, проверить итерации в UI |
| Товар «висит» в воздухе после удара | нет гравитации для эффектора | эффектор по умолчанию статичен — либо кинематика, либо внешняя анимация |
🔗 СВЯЗИ
- Прочие в категории «Воркфлоу»: WF 12: CROWD SIM GN, WF 13: DESTRUCTION RBD, WF 01: TEXT-TO-SCENE, WF 02: IMAGE-TO-3D, WF 03: TEXTURE PIPELINE
- По темам: P21: PHYSICS SIMULATION, P45: GEOMETRY NODES SIMULATION ZONES, P47: ALEMBIC CACHE PIPELINE, SIMULATION & PHYSICS: GEOMETRY NODES 4.0+
- Теги: #simulation, #workflow
Связанные материалы
Подобраны по тегам и категории.