文章

DST Particle Emitter 工具

Particle 工具自用

Emitters

xz平面环形指定角度范围

1
2
3
4
5
6
7
8
9
10
11
12
13
local function CreateRingEmitterWithAngle( radius, angle_start, angle_end )
    local cos = math.cos
    local sin = math.sin
    local rad = math.rad
    local random = math.random

    return function()
        local angle = rad(angle_start) + random() * rad(angle_end - angle_start)
        local x = radius * cos(angle)
        local y = radius * sin(angle)
        return x, y
    end
end

Y平面环形指定角度范围 圆环始终朝向相机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
local function CreateCircleEmitterOnYAxisProjection(radius, angle_start, angle_end)
    local rand = math.random
    local sin = math.sin
    local cos = math.cos
    local rad = math.rad

    return function()
        local cameraAngle = (TheCamera:GetHeadingTarget() - 90) * DEGREES
        -- Generate a random angle for the circle
        local t = 2.0 * PI * rand()
        if angle_start and angle_end then
            t = rad(angle_start) + rand() * rad(angle_end - angle_start)
        end
        -- Compute the coordinates on the circle in the y-z plane
        local y = radius * cos(t)
        local z = radius * sin(t)
        -- Rotate the point based on the camera angle
        local rotatedX = z * cos(cameraAngle)
        local rotatedZ = z * sin(cameraAngle)
        -- Return the coordinates on the projected circle
        return rotatedX, y, rotatedZ
    end
end

Tools

生成可用UV offset 的 TEXTURE

导入自己的TEXTURE时需要resolvefilepath(“fx/my_texture.tex”) 加载素材Asset(“IMAGE”, TEXTURE) 不需要xml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from PIL import Image
import os

def resize_and_pad_image(image_path, target_size=(64, 64)):
    img = Image.open(image_path).convert("RGBA")
    bbox = img.getbbox()
    if bbox:
        img = img.crop(bbox)
    img.thumbnail(target_size, Image.LANCZOS)
    
    new_img = Image.new("RGBA", target_size, (0, 0, 0, 0))
    img_w, img_h = img.size
    new_img.paste(img, ((target_size[0] - img_w) // 2, (target_size[1] - img_h) // 2))
    
    return new_img

def concatenate_images(image_list, output_path):
    width = 64 * len(image_list)
    height = 64
    new_img = Image.new("RGBA", (width, height), (0, 0, 0, 0))
    
    for i, img in enumerate(image_list):
        new_img.paste(img, (i * 64, 0))
    
    new_img.save(output_path)

def process_images(image_paths, output_path):
    resized_images = [resize_and_pad_image(img_path) for img_path in image_paths]
    concatenate_images(resized_images, output_path)

# input_folder下的png调整到64x64并拼接到宽
if __name__ == "__main__":
    input_folder = "./raw"
    output_image_path = "./image.png"
    
    image_paths = [os.path.join(input_folder, fname) for fname in os.listdir(input_folder) if fname.endswith('.png')]
    process_images(image_paths, output_image_path)
1
2
3
4
5
6
7
@echo off
set PYTHON_EXECUTABLE="D:\path\to\python.exe"

set PYTHON_SCRIPT_PATH="script.py"

%PYTHON_EXECUTABLE% %PYTHON_SCRIPT_PATH%
pause

Samples

从图片生成粒子坐标

Sample Image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
--------------------------------------------------------------------------

local TEXTURE = "fx/wintersnow_cane.tex"
local SHADER = "shaders/vfx_particle.ksh"

local COLOUR_ENVELOPE_NAME = "testparticle_colourenvelope"
local SCALE_ENVELOPE_NAME = "testparticle_scaleenvelope"

local skull = require("imggriddata").skull

local assets =
{
    Asset("IMAGE", TEXTURE),
    Asset("SHADER", SHADER),
}

local function CreateImageEmitter(grid_data, grid_width, grid_size_each)
    return function ()
        local index = grid_data[math.random(#grid_data)]
        local y = math.floor(index / grid_width)
        local x = index % grid_width
        return (x - grid_width / 2) * grid_size_each, (y - grid_width / 2) * grid_size_each
    end
end

--------------------------------------------------------------------------
local function IntColour(r, g, b, a)
    return { r / 255, g / 255, b / 255, a / 255 }
end

local function InitEnvelope()

    EnvelopeManager:AddColourEnvelope(COLOUR_ENVELOPE_NAME,
        {
            { 0, IntColour(255, 255, 255, 255) },
            { 0.5, IntColour(255, 255, 255, 255) },
            { 1, IntColour(255, 255, 255, 0) },
        }
    )

    local max_scale = 1
    local end_scale = 1

    local envs = {
        { 0, { max_scale, max_scale } },
        { 1, { max_scale * end_scale, max_scale * end_scale } }
    }

    EnvelopeManager:AddVector2Envelope( SCALE_ENVELOPE_NAME, envs )

    InitEnvelope = nil
    IntColour = nil
end
--------------------------------------------------------------------------
local MAX_LIFETIME = 1

local function emit_fn(effect, i, emitter)
    local vx, vy, vz = 0, 0, 0
    local lifetime = MAX_LIFETIME * (.6 + math.random() * .4)
    local px, py, pz = emitter()
    if pz == nil then
        pz = py
        py = 0
    end

    local uv_offset = math.random(0, 7) * .125

    effect:AddParticleUV(
        i,
        lifetime,           -- lifetime
        px, py, pz,         -- position
        vx, vy, vz,         -- velocity
        uv_offset, 0        -- uv offset
    )
end

local function fn()
    local inst = CreateEntity()

    inst.entity:AddTransform()
    inst.entity:AddNetwork()

    inst:AddTag("FX")

    inst.entity:SetPristine()

    inst.persists = false

    --Dedicated server does not need to spawn local particle fx
    if TheNet:IsDedicated() then
        return inst
    elseif InitEnvelope ~= nil then
        InitEnvelope()
    end

    local effect = inst.entity:AddVFXEffect()
    effect:InitEmitters(1)

    effect:SetRenderResources(0, TEXTURE, SHADER)
    -- Rotation Status must match `effect:Add[Rotating]ParticleUV`
    -- effect:SetRotationStatus(0, true)
    effect:SetUVFrameSize(0, .125, 1)
    effect:SetMaxNumParticles(0, 1024)
    effect:SetMaxLifetime(0, MAX_LIFETIME)
    effect:SetColourEnvelope(0, COLOUR_ENVELOPE_NAME)
    effect:SetScaleEnvelope(0, SCALE_ENVELOPE_NAME)
    effect:SetBlendMode(0, BLENDMODE.Premultiplied)
    effect:EnableBloomPass(0, true)
    effect:SetSortOrder(0, 0)
    effect:SetSortOffset(0, 1)

    -----------------------------------------------------
    local tick_time = TheSim:GetTickTime()

    local desired_pps = 500
    local particles_per_tick = desired_pps * tick_time
    local num_particles_to_emit = 0

    local emitter = CreateImageEmitter(skull, 128, .05)

    EmitterManager:AddEmitter(inst, nil, function()

        while num_particles_to_emit > 1 do
            emit_fn(effect, 0, emitter)
            num_particles_to_emit = num_particles_to_emit - 1
        end
        num_particles_to_emit = num_particles_to_emit + particles_per_tick * math.random()
    end)

    return inst
end

return Prefab("testparticle", fn, assets)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from PIL import Image

def resize_image_with_padding(input_image_path, output_image_path, max_size=256):
    # Open an image file
    with Image.open(input_image_path) as image:
        width, height = image.size
        
        # Calculate the new dimensions preserving the aspect ratio
        if width > height:
            new_width = max_size
            new_height = int((max_size / width) * height)
        else:
            new_height = max_size
            new_width = int((max_size / height) * width)
        
        # Resize the image
        resized_image = image.resize((new_width, new_height), Image.LANCZOS)
        
        # Create a new image with white background
        new_image = Image.new("RGB", (max_size, max_size), (255, 255, 255))
        
        # Calculate the position to paste the resized image on the white background
        upper_left_x = (max_size - new_width) // 2
        upper_left_y = (max_size - new_height) // 2
        
        # Paste the resized image onto the white background
        new_image.paste(resized_image, (upper_left_x, upper_left_y))
        
        # Save the new image
        new_image.save(output_image_path)
        print(f"Image has been resized, padded, and saved as {output_image_path}")

input_image_path = "./skull.jpg"
output_image_path = "skull_resized_padded.jpg"
resize_image_with_padding(input_image_path, output_image_path, 128)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from PIL import Image
import numpy as np

def process_image(input_image_path, output_image_path, lua_file_path):
    # 读取图片并转换为灰度图像
    image = Image.open(input_image_path).convert('L')
    
    # 二值化处理,找到所有黑色像素点
    threshold = 128
    image = image.point(lambda p: p > threshold and 255 or 0)
    
    # 保存二值化后的图片
    image.save(output_image_path)
    
    # 将图片转换为NumPy数组以便处理
    image_array = np.array(image)
    
    # 找到所有黑色像素点(即值为0的点)
    black_points = np.transpose(np.nonzero(image_array == 0))
    
    # 获取图片宽度
    width = image_array.shape[1]
    
    # 计算偏移量(转换为从0开始的序号)
    offsets = [point[0] * width + point[1] for point in black_points]
    
    # 将偏移量保存到Lua表中
    with open(lua_file_path, 'w') as lua_file:
        lua_file.write('black_pixel_indices = {\n')
        for offset in offsets:
            lua_file.write(f"    {offset},\n")
        lua_file.write('}\n')

    print(f"Binary image saved to {output_image_path}, offsets saved to {lua_file_path}")

# 使用示例
input_image_path = 'skull_resized_padded.jpg'
output_image_path = 'output_binary.jpg'
lua_file_path = 'pixels.lua'
process_image(input_image_path, output_image_path, lua_file_path)
  1. resize.py 将图片进行二值化和缩放处理
  2. togrid.py 保存图像有效像素点下标
  3. testparticle.lua 读取像素点下标并解析得到坐标
  4. testparticle.lua 选择随机坐标生成粒子
本文由作者按照 CC BY 4.0 进行授权

热门标签