|
config.ParticlesMaxBaseColorMapSize = 2048 |
|
config.ParticlesMaxNormalMapSize = 512 |
|
|
|
local function folder_fn(obj) |
|
return obj:GetTextureFolders() |
|
end |
|
|
|
local function filter_fn_texture(obj) |
|
return obj:GetTextureFilter() |
|
end |
|
|
|
local function filter_fn_normalmap(obj) |
|
return obj:GetNormalmapFilter() |
|
end |
|
|
|
DefineClass.ParticleEmitter = |
|
{ |
|
__parents = { "ParticleBehavior" }, |
|
PropEditorCopy = true, |
|
EditorName = "Emitter", |
|
|
|
properties = { |
|
|
|
{ id = "emit_detail_level", name = "Detail level category", category = "Base", default = ActionFXDetailLevelCombo()[1].value, editor = "combo", items = ActionFXDetailLevelCombo, help = "Determines the options detail levels at which the emitter is active. Essential will make it always active, Optional at high/medium setting, and EyeCandy at high setting only.", }, |
|
|
|
{ id = "enabled", name = "Enabled", category = "Emission Attributes", editor = "bool", dynamic = true, }, |
|
{ id = "emit_fade", name = "Emit fading range (m)", category = "Emission Attributes", editor = "range", slider = true, min = 0, max = 15000, default = range( 0, 2000 ), help = "Camera distance interval in which particles will be emitted." }, |
|
{ id = "max_live_count", name = "Max live count", category = "Emission Attributes", editor = "number", min = 1, dynamic = true, max = 5000 }, |
|
{ id = "parts_per_sec", name = "Particles/sec", category = "Emission Attributes", editor = "number", scale = 100, dynamic = true, max = 12000000 }, |
|
{ id = "parts_per_meter", name = "Particles/meter", category = "Emission Attributes", editor = "number", scale = 100, dynamic = true }, |
|
{ id = "parts_per_meter_min_velocity", name = "Particles/meter treshold", category = "Emission Attributes", editor = "number", scale = 1000, default = 0, }, |
|
{ id = "lifetime_min", name = "Lifetime min", category = "Emission Attributes", editor = "number", min = 0, scale = "sec", dynamic = true }, |
|
{ id = "lifetime_max", name = "Lifetime max", category = "Emission Attributes", editor = "number", min = 0, scale = "sec", dynamic = true }, |
|
{ id = "position", name = "Position", category = "Emission Attributes", editor = "point", scale = guim, dynamic = true }, |
|
{ id = "angle", name = "Angle (degrees)", category = "Emission Attributes", editor = "range", slider = true, min = -360, max = 360 }, |
|
{ id = "size_min", name = "Size min (m)", category = "Emission Attributes", editor = "number", min = 1, scale = 1000, dynamic = true }, |
|
{ id = "size_max", name = "Size max (m)", category = "Emission Attributes", editor = "number", min = 1, scale = 1000, dynamic = true }, |
|
{ id = "part_emit_modifier", name = "Emit Modifier", category = "Emission Attributes", editor = "number", min = 1, scale = 1000, dynamic = true }, |
|
|
|
{ id = "geometry_building", name = "Geometry", category = "Material Attributes", editor = "combo", items = { "Particle", "Ribbon", "Decal" }, help = "What type of geometry is being emitted." }, |
|
{ id = "decal_depth", name = "Decal Depth", category = "Material Attributes", editor = "number", scale = 1000, no_edit = function(self) return self.geometry_building ~= "Decal" end, dynamic = true }, |
|
{ id = "decal_group", name = "Decal Group", category = "Material Attributes", editor = "choice", items = const.DecalGroups, no_edit = function(self) return self.geometry_building ~= "Decal" end, dynamic = true }, |
|
{ id = "shader", name = "Shader", category = "Material Attributes", editor = "combo", items = { "Solid", "Blend", "Add", "Premultiplied", "Overlay", "Add Light", "Distortion", "Blend Exposed" } }, |
|
{ id = "SourceTexture", name = "Texture", category = "Material Attributes", default = false, editor = "browse", folder = folder_fn, filter = filter_fn_texture, dont_save = true, }, |
|
{ id = "texture", editor = "text", no_edit = true, }, |
|
{ id = "TexturePreview", name = "Texture Preview", category = "Material Attributes", editor = "image", default = "", dont_save = true, |
|
img_size = 128, img_box = 1, img_comp = "rgba", img_back = RGB(0, 0, 0), |
|
img_polyline_color = RGB(128, 128, 128), img_polyline = function(self) return self.outlines end, img_polyline_closed = true }, |
|
{ id = "filtering_bilinear", name = "Texture filtering: smooth", category = "Material Attributes", editor = "bool" }, |
|
{ id = "normal_as_flow_map", name = "Normal map as Flow map", category = "Material Attributes", editor = "bool", default = false, no_edit = function(self) return self.shader == "Distortion" end, dynamic = true }, |
|
{ id = "SourceNormalmap", name = "Normalmap", category = "Material Attributes", editor = "browse", folder = folder_fn, filter = filter_fn_normalmap, image_preview_size = 128, dont_save = true, default = false }, |
|
{ id = "normalmap", editor = "text", no_edit = true, }, |
|
{ id = "frames", name = "UV Frames", category = "Material Attributes", editor = "point2d", max = 8*8, help = "X * Y should be limited to 6x6" }, |
|
{ id = "self_illum", name = "Self-Illumination", category = "Material Attributes", editor = "number", slider = true, min = 0, max = 200, no_edit = function(self) return self.shader == "Distortion" end, dynamic = true, help = "Shaded particles receive additional light based on the material color." }, |
|
{ id = "light_softness", name = "NM Light Softness", category = "Material Attributes", editor = "number", slider = true, min = 0, max = 1000, no_edit = function(self) return self.shader == "Distortion" end, dynamic = true, help = "How soft should the light affecting this particle be" }, |
|
{ id = "mat_roughness", name = "Material Roughness", category = "Material Attributes", editor = "number", min = 0, max = 100, slider = true, no_edit = function(self) return self.shader == "Distortion" end, dynamic = true, help = "Controls the roughness of the material surface. Value of 1 results in no specular reflection and an optimized shading path." }, |
|
{ id = "mat_metallic", name = "Material Metallic", category = "Material Attributes", editor = "number", min = 0, max = 100, slider = true, no_edit = function(self) return self.shader == "Distortion" end, dynamic = true, help = "Controls the metallicness of the material surface." }, |
|
{ id = "receive_shadow", name = "Receive Shadow", category = "Material Attributes", editor = "bool", default = false }, |
|
|
|
{ id = "flow_speed", name = "Flow Speed", category = "Material Attributes", editor = "number", slider = true, min = 0, max = 1000, default = 0, help = "Speed of texture distortion", no_edit = function(self) return not self.normal_as_flow_map end, dynamic = true }, |
|
{ id = "flow_scale", name = "Flow Scale", category = "Material Attributes", editor = "number", slider = true, min = 0, max = 1000, default = 0, help = "Scale of texture distortion", no_edit = function(self) return not self.normal_as_flow_map end, dynamic = true }, |
|
|
|
{ id = "softness", name = "Softness (m)", no_edit = true, category = "Transparency Attributes", editor = "number", scale = 100, read_only = function(obj, prop_meta) return obj.ui end }, |
|
{ id = "far_softness", name = "Terrain Distance Fade (m)", category = "Transparency Attributes", editor = "number", scale = 100, min = -1, read_only = function(obj, prop_meta) return obj.ui end }, |
|
{ id = "near_softness", name = "Camera Distance Fade (m)", category = "Transparency Attributes", editor = "number", scale = 100, min = -1, read_only = function(obj, prop_meta) return obj.ui end }, |
|
{ id = "far_softness_curve", name = "Far Softness %", category = "Transparency Attributes", editor = "curve4", fixedx = true, scale = 1000, min = 0, max = 1000, default = MakeLine(0, 1000), }, |
|
{ id = "near_softness_curve", name = "Near Softness %", category = "Transparency Attributes", editor = "curve4", fixedx = true, scale = 1000, min = 0, max = 1000, default = MakeLine(0, 1000), }, |
|
{ id = "viewangle_softness_curve", name = "View Angle Softness %", category = "Transparency Attributes", editor = "curve4", fixedx = true, scale = 1000, min = 0, max = 1000, default = MakeLine(1000, 1000), }, |
|
|
|
{ id = "view_dependent_opacity", name = "View dependent opacity", category = "Transparency Attributes", editor = "number", slider = true, min = 0, max = 1000, default = 0, help = "Lowers particle opacity when viewed perpendicular to normal." }, |
|
{ id = "alpha_test", name = "Alpha test (0-255)", category = "Transparency Attributes", editor = "number", slider = true, min = 0, max = 255, help = "Cut texture pixels with lesser alpha" }, |
|
{ id = "alpha", name = "Alpha (0-255)", category = "Transparency Attributes", editor = "range", slider = true, min = 0, max = 255 }, |
|
{ id = "interior", category = "Transparency Attributes", editor = "bool", default = true, help = "Render if inside buildings" }, |
|
{ id = "exterior", category = "Transparency Attributes", editor = "bool", default = true, help = "Render if outside buildings"}, |
|
|
|
{ id = "normal_to_distortion", name = "Normal map as Distortion map", category = "Material Attributes", editor = "bool", no_edit = function(self) return self.shader ~= "Distortion" end, dynamic = true }, |
|
{ id = "distortion_mode", name = "Distortion mode", category = "Material Attributes", editor = "combo", items = { "Fixed", "Ramp", "Ping-Pong" }, help = "How does distortion change overt time", no_edit = function(self) return self.shader ~= "Distortion" end, dynamic = true }, |
|
{ id = "distortion_start", name = "Distortion start", category = "Material Attributes", editor = "number", scale = "sec", help = "When does the animated distortion effect start", no_edit = function(self) return self.shader ~= "Distortion" end, dynamic = true }, |
|
{ id = "distortion_end", name = "Distortion end", category = "Material Attributes", editor = "number", scale = "sec", help = "When does the animated distortion effect end", no_edit = function(self) return self.shader ~= "Distortion" end, dynamic = true }, |
|
{ id = "distortion_scale", name = "Distortion scale (min)", category = "Material Attributes", editor = "number", min = -1000, max = 1000, slider = true, help = "The strength of the distortion effect", no_edit = function(self) return self.shader ~= "Distortion" end, dynamic = true }, |
|
{ id = "distortion_scale_max", name = "Distortion scale (max)", category = "Material Attributes", editor = "number", min = -1000, max = 1000, slider = true, help = "The strength of the distortion effect", no_edit = function(self) return self.shader ~= "Distortion" end, dynamic = true }, |
|
|
|
{ id = "no_depth_test", name = "No depth test", category = "Depth Settings", editor = "bool", default = false, read_only = function(obj, prop_meta) return obj.ui end }, |
|
{ id = "drawing_order", name = "Drawing order", category = "Depth Settings", editor = "number" }, |
|
{ id = "depth_offset", name = "Depth offset", category = "Depth Settings", editor = "number", min = -10000, max = 10000, scale = "m", slider = true, help = "Camera relative offset of particles along the view direction" }, |
|
{ id = "sort", name = "Sort particles", category = "Depth Settings", editor = "bool", help = "Should we sort the particles front to back to the camera or not (Blend mode only)!" }, |
|
|
|
{ id = "probability", no_edit = true }, |
|
{ id = "outlines", default = false, editor = "prop_table", no_edit = true }, |
|
{ id = "texture_hash", default = false, editor = "number", no_edit = true }, |
|
{ id = "ui", default = false, editor = "bool", no_edit = false }, |
|
{ id = "mat_ice", name = "Material Ice", category = "Material Attributes", editor = "number", min = 0, max = 100, slider = true, no_edit = true, help = "Controls how much the material is affected by the Ice special effect." }, |
|
}, |
|
|
|
bins = set( "A" ), |
|
geometry_building = "Particle", |
|
time_start = 0, |
|
time_stop = -1000, |
|
time_period = 0, |
|
randomize_period = 0, |
|
enabled = true, |
|
max_live_count = 200, |
|
parts_per_sec = 5000, |
|
parts_per_meter = 0, |
|
lifetime_min = 1000, |
|
lifetime_max = 3000, |
|
texture = "", |
|
normalmap = "", |
|
self_illum = 0, |
|
light_softness = 800, |
|
decal_depth = 1000, |
|
decal_group = "Default", |
|
part_emit_modifier = 1000, |
|
|
|
frames = point(1, 1), |
|
drawing_order = 0, |
|
softness = 0, |
|
far_softness = -1, |
|
near_softness = -1, |
|
alpha_test = 0, |
|
shader = "Blend", |
|
position = point30, |
|
size_min = 1000, |
|
size_max = 2000, |
|
alpha = range( 255, 255 ), |
|
angle = range( 0, 0 ), |
|
filtering_bilinear = true, |
|
sort = true, |
|
normal_to_distortion = false, |
|
distortion_mode = "Fixed", |
|
distortion_scale = 0, |
|
distortion_scale_max = 0, |
|
distortion_start = 0, |
|
distortion_end = 0, |
|
velocity_min = 0, |
|
velocity_max = 0, |
|
mat_roughness = 100, |
|
mat_metallic = 0, |
|
mat_ice = 100, |
|
depth_offset = 0, |
|
} |
|
|
|
function ParticleEmitter:GetTextureFolders() |
|
return GetParentTable(self):GetTextureFolders() |
|
end |
|
|
|
function ParticleEmitter:GetTextureFilter() |
|
return "Texture (*.tga)|*.tga" |
|
end |
|
|
|
function ParticleEmitter:GetNormalmapFilter() |
|
return "Texture (*.norm.tga)|*.norm.tga" |
|
end |
|
|
|
function ParticleEmitter:Setframes(value) |
|
local x, y = value:xy() |
|
local max = table.find_value(self.properties, "id", "frames").max |
|
local frames = x * y |
|
if frames > max then |
|
x = max * x / frames |
|
y = max * y / frames |
|
assert(x * y <= max, "Frames not properly limit-scaled down") |
|
end |
|
self.frames = point(x, y) |
|
end |
|
|
|
function ParticleEmitter:GetColorForGed() |
|
return self.enabled and "32 128 32" or "169 18 23" |
|
end |
|
|
|
function ParticleEmitter:ShouldNormalizeTexturePath() |
|
return true |
|
end |
|
|
|
local function ConvertSlashes(path) |
|
|
|
return string.gsub(path, "\\", "/") |
|
end |
|
|
|
local function EscapeMagicSymbols(path) |
|
|
|
return string.gsub(ConvertSlashes(path), "[%(%)%.%%%+%-%*%?%[%^%$]", "%%%1") |
|
end |
|
|
|
function ParticleEmitter:NormalizeTexturePath(texture_path) |
|
if not self:ShouldNormalizeTexturePath() then return texture_path end |
|
|
|
return texture_path |
|
end |
|
|
|
function ParticleEmitter:Settexture(texture) |
|
self.texture = self:NormalizeTexturePath(texture) |
|
end |
|
|
|
function ParticleEmitter:Setnormalmap(texture) |
|
self.normalmap = self:NormalizeTexturePath(texture) |
|
end |
|
|
|
function ParticleEmitter:GetAlphaPreview() |
|
return self.texture |
|
end |
|
|
|
function ParticleEmitter:GetTexturePreview() |
|
return self.texture |
|
end |
|
|
|
local function GetSegments(path) |
|
path = path:gsub("[\\/]+", '/') |
|
local segments = {} |
|
for segment in string.gmatch(path, '[^/]+') do |
|
table.insert(segments, segment) |
|
end |
|
return segments |
|
end |
|
|
|
local function GetRelativePath(path, base, game_path) |
|
if not path then return path end |
|
path = GetSegments(path) |
|
base = GetSegments(base) |
|
game_path = GetSegments(game_path) |
|
|
|
for key, value in ipairs(base) do |
|
if value ~= path[key] then |
|
return false |
|
end |
|
end |
|
|
|
local moved = table.move(path, #base + 1, #path, #game_path + 1, game_path) |
|
return table.concat(moved, '/') |
|
end |
|
|
|
function ParticleEmitter:GetTextureBasePath() |
|
return GetParentTable(self):GetTextureBasePath() |
|
end |
|
|
|
function ParticleEmitter:GetTextureTargetPath() |
|
return GetParentTable(self):GetTextureTargetPath() |
|
end |
|
|
|
function ParticleEmitter:GetTextureTargetGamePath() |
|
return GetParentTable(self):GetTextureTargetGamePath() |
|
end |
|
|
|
function ParticleEmitter:GetSourceTexture() |
|
if self.texture == "" or not self.texture then |
|
return "" |
|
end |
|
return self:GetTextureBasePath() .. self.texture |
|
end |
|
|
|
function ParticleEmitter:SetSourceTexture(value) |
|
self.texture = GetRelativePath(value, self:GetTextureBasePath() .. self:GetTextureTargetPath(), self:GetTextureTargetGamePath()) or "" |
|
end |
|
|
|
function ParticleEmitter:GetSourceNormalmap() |
|
if self.normalmap == "" or not self.normalmap then |
|
return "" |
|
end |
|
return self:GetTextureBasePath() .. self.normalmap |
|
end |
|
|
|
function ParticleEmitter:SetSourceNormalmap(value) |
|
self.normalmap = GetRelativePath(value, self:GetTextureBasePath() .. self:GetTextureTargetPath(), self:GetTextureTargetGamePath()) or "" |
|
end |
|
|
|
|
|
function ParticleEmitter:ScheduleGenerateOutlines() |
|
self.outlines = false |
|
CreateRealTimeThread(self.GenerateOutlines, self) |
|
end |
|
|
|
function ParticleEmitter:Setui(ui) |
|
self.ui = ui |
|
if ui then |
|
self.no_depth_test = true |
|
self.softness = 0 |
|
end |
|
end |
|
|
|
local outlines_cache = false |
|
local texture_to_hash = false |
|
function ClearOutlinesCache() |
|
outlines_cache = false |
|
texture_to_hash = false |
|
end |
|
|
|
function ParticleEmitter:GenerateOutlines(update_mode) |
|
if self.outlines and not update_mode then |
|
return |
|
end |
|
local texture = self.texture |
|
texture_to_hash = texture_to_hash or {} |
|
local texture_hash = texture_to_hash[texture] |
|
if texture_hash == nil then |
|
local err |
|
if texture ~= "" then |
|
err, texture_hash = AsyncFileToString(self:GetSourceTexture(), nil, nil, "hash") |
|
if err then |
|
print("Error", err, "while computing hash of", texture) |
|
end |
|
end |
|
texture_hash = texture_hash or false |
|
texture_to_hash[texture] = texture_hash |
|
end |
|
if update_mode == "update" and texture_hash == self.texture_hash then |
|
return |
|
end |
|
local outlines, generated |
|
if self.normal_to_distortion then |
|
outlines = {} |
|
else |
|
local param_hash = xxhash(texture, self.alpha_test, self.frames) |
|
outlines_cache = outlines_cache or {} |
|
outlines = outlines_cache[param_hash] |
|
if not outlines then |
|
generated = true |
|
local err |
|
err, outlines = TrimParticleTexture( texture, self.alpha_test, self.frames:x(), self.frames:y() ) |
|
outlines = outlines or {} |
|
if err then |
|
print("Outlines:", err) |
|
end |
|
outlines_cache[param_hash] = outlines |
|
local count = 0 |
|
for i=1,#outlines do |
|
count = count + #outlines[i] |
|
end |
|
if count > 256 then |
|
print("Too many outlines", count, "in", texture) |
|
end |
|
end |
|
end |
|
self.texture_hash = texture_hash |
|
self.outlines = outlines |
|
ObjModified(self) |
|
return true, generated |
|
end |
|
|
|
function ParticleEmitter:IsOutlineProp(prop_id) |
|
local outline_props = { |
|
normal_to_distortion = true, |
|
texture = true, |
|
SourceTexture = true, |
|
frames = true, |
|
alpha_test = true, |
|
} |
|
return outline_props[prop_id] |
|
end |
|
|
|
|
|
function ParticleEmitter:OnEditorSetProperty(prop_id, old_value, ged) |
|
local value = self:GetProperty(prop_id) |
|
if prop_id == "texture" then |
|
local w, h = UIL.MeasureImage(value) |
|
if (Max(w, h) > config.ParticlesMaxBaseColorMapSize) then |
|
ged:ShowMessage("Warning", "Diffuse texture is over the size limit " .. config.ParticlesMaxBaseColorMapSize .. "px. Please resize!") |
|
end |
|
elseif prop_id == "normalmap" then |
|
local w, h = UIL.MeasureImage(value) |
|
if (Max(w, h) > config.ParticlesMaxNormalMapSize) then |
|
ged:ShowMessage("Warning", "Normalmap texture is over the size limit " .. config.ParticlesMaxNormalMapSize .. "px. Please resize!") |
|
end |
|
elseif prop_id == "emit_detail_level" then |
|
if type(value) == "string" and tonumber(value) then |
|
self.emit_detail_level = tonumber(value) |
|
end |
|
end |
|
|
|
if self:IsOutlineProp(prop_id) then |
|
self:ScheduleGenerateOutlines() |
|
end |
|
end |
|
|
|
function ParticleEmitter:GetError() |
|
if self.emit_detail_level == ActionFXDetailLevelCombo()[1].value then |
|
return "Please set the 'Detail level category' property to specify at which detail levels this emitter should be active." |
|
end |
|
end |
|
|
|
function ParticleEmitter:OnEditorNew(preset, ged, is_paste) |
|
self:Setui(preset.ui) |
|
preset:OverrideEmitterFuncs(self) |
|
end |
|
|
|
function SaveParticleSystem(parsys) |
|
parsys:EnableDynamicToggles() |
|
parsys:Save() |
|
end |
|
|
|
function ReloadParticleTexture(filename) |
|
local parsyslist = GetParticleSystemList() |
|
for i=1,#parsyslist do |
|
local parsys = parsyslist[i] |
|
for j=1,#parsys do |
|
local behavior = parsys[j] |
|
if behavior:IsKindOf("ParticleEmitter") and behavior.texture == filename then |
|
DelayedCall( 500, SaveParticleSystem, parsys ) |
|
end |
|
end |
|
end |
|
end |
|
|
|
DefineClass.Rename = |
|
{ |
|
__parents = { "ParticleBehavior" }, |
|
|
|
properties = |
|
{ |
|
{ id = "life_time", name = "Lifetime (sec)", editor = "number", min = 100, scale = 1000, dynamic = true }, |
|
{ id = "remove_bins", name = "Remove Bins", editor = "set", items = { "A", "B", "C", "D", "E", "F", "G", "H" } }, |
|
{ id = "add_bins", name = "Add Bins", editor = "set", items = { "A", "B", "C", "D", "E", "F", "G", "H" } }, |
|
{ id = "reset_new_flag", name = "Reset New Flag", editor = "number" }, |
|
}, |
|
|
|
EditorName = "Rename", |
|
|
|
life_time = 1000, |
|
remove_bins = set(), |
|
add_bins = set(), |
|
reset_new_flag = 0, |
|
} |
|
|
|
if Platform.developer then |
|
|
|
function FindParticleMaxProperty(part_class, prop) |
|
local particles = GetParticleSystemList() |
|
local max |
|
for i = 1, #particles do |
|
local par_sys = particles[i] |
|
for k = 1, #par_sys do |
|
local par_beh = par_sys[k] |
|
if par_beh:IsKindOf(part_class) then |
|
|
|
|
|
local value = par_beh:GetProperty(prop) |
|
value = type(value) == "number" and value or 0 |
|
if not max or value > max then |
|
max = value |
|
end |
|
end |
|
end |
|
end |
|
|
|
return max |
|
end |
|
|
|
function DumpPartStats() |
|
print("Max ParticleEmitter stats:") |
|
print("Frames: ", FindParticleMaxProperty("ParticleEmitter", "frames")) |
|
print("Max Live Count: ", FindParticleMaxProperty("ParticleEmitter", "max_live_count")) |
|
print("Particles/Sec: ", FindParticleMaxProperty("ParticleEmitter", "parts_per_sec")) |
|
end |
|
|
|
end |