솔리드 모델링

*이 콘텐츠는 AI(베타)를 사용해 번역되었으며, 오류가 있을 수 있습니다. 이 페이지를 영어로 보려면 여기를 클릭하세요.

솔리드 모델링은 BaseParts를 독특한 방식으로 결합하여 더 복잡한 형태를 형성하는 과정입니다. 여기에는 일반적으로 Constructive Solid Geometry (CSG)로 알려진 불리언 연산인 합집합, 교차, 차감이 포함됩니다. 스튜디오, 플러그인, 게임 내에서 서버와 클라이언트 모두에서 솔리드 모델링을 수행할 수 있습니다.

불리언 CSG 외에도, 솔리드 모델링은 물에 잠기지 않는 메시를 지원하며, 스윕 및 파편화와 같은 작업을 통해 플레이어가 기하학을 자르고 쪼개어 독특한 게임 플레이 상호작용을 할 수 있도록 합니다.

MeshPart가 SweepPartAsync()로 생성됨
MeshPart가 FragmentAsync()로 파손됨

물에 잠기지 않는 기하학

솔리드 모델링 작업은 물에 잠기지 않는 기하학에서만 작동할 수 있습니다. 기술적으로, 메시가 물에 잠기지 않는다는 것은 닫혀 있고, 다면체이며, 자기 교차하지 않는 것을 의미합니다. 이러한 용어는 엄격한 정의를 가지고 있지만, 다음은 몇 가지 간단한 규칙입니다:

  • 각 면은 "안쪽"과 "바깥쪽"이 있어야 합니다. 이는 면의 세 개의 정점의 순서에 의해 결정됩니다.
  • 각 모서리는 정확히 두 개의 삼각형에 의해 공유되어야 합니다. 이는 메시의 구멍 주위의 모서리가 하나의 삼각형만 가질 수 있기 때문에 구멍이 없어야 함을 의미합니다.
  • 면은 다른 면을 통과할 수 없습니다.
  • 인접한 삼각형은 "바깥쪽"이 어떤 쪽인지에 대해 동의해야 합니다.
  • 각 정점은 정확히 하나의 인접 삼각형의 팬을 가져야 합니다.

다음 예제는 언급된 이유로 물에 잠기지 않는 것입니다:

솔리드 모델링 시스템은 메시의 특정 작은 문제를 자동으로 수리할 수 있지만, 일반적으로 메시가 물에 잠기지 않으면 API 호출이 실패합니다. 기존의 물에 잠기지 않는 메시를 수리하는 방법은 일률적이지 않지만, 3D Print Toolbox 및 Mesh Repair Tools와 같은 여러 Blender 플러그인이 도움이 될 수 있습니다. 또 다른 옵션으로는, Meshlab도 메시를 다면체로 만들기 위해 매우 유용한 도구를 내장하고 있습니다. 이는 메시가 물에 잠기기 위한 주요 요구 사항입니다.

메시가 물에 잠기지 않도록 만드는 것이 매우 어려운지 확인하는 한 가지 방법은 스튜디오에서 모든 각도에서 메시를 살펴본 다음, 메시의 MeshPart.DoubleSided 속성을 활성화 및 비활성화해보는 것입니다. 차이를 볼 수 있다면, 메시가 단순한 껍데기일 뿐이며, 위에서 언급한 도구는 메시의 내부와 외부를 추측할 수 없기 때문에 작동하지 않을 것입니다. 그러나 얇은 메시만 원하고 메시의 치수를 정확히 유지하는 것이 중요하지 않다면, Blender의 Solidify modifier를 사용하여 껍데기를 약간 두껍게 하여 물에 잠기는 메시로 만들 수 있습니다.

DoubleSided = false

스튜디오에서의 솔리드 모델링

모델 탭 도구 모음 내에서 네 가지 도구를 사용하여 세 가지 기본 불리언 작업을 수행할 수 있습니다.

스튜디오 도구 모음에서 강조된 솔리드 모델링 도구.
도구단축키설명
합집합ShiftCtrlG (Windows)
Shift⌘G (Mac)
두 개 이상의 부품을 결합하여 단일 솔리드 합집합을 형성합니다.
교차ShiftCtrlI (Windows)
Shift⌘I (Mac)
겹치는 부품을 단일 솔리드 교차로 교차합니다.
분리ShiftCtrlU (Windows)
Shift⌘U (Mac)
합집합 또는 교차를 개별 부품으로 다시 분리합니다.
차감ShiftCtrlN (Windows)
Shift⌘N (Mac)
부품을 차감하여 구멍과 움푹 들어간 부분을 만드는 데 유용합니다.

합집합

합집합 도구는 두 개 이상의 부품을 결합하여 단일 솔리드 UnionOperation을 형성합니다.

블록과 실린더.
개별 부품
하나의 객체로 결합된 블록과 실린더.
합집합 결과

부품을 합집합으로 결합하려면:

  1. 결합하려는 모든 부품을 선택합니다.
  2. 합집합 버튼을 클릭합니다. 모든 부품이 하나의 솔리드 UnionOperation으로 결합되며 이름은 합집합입니다.

교차

교차 도구는 겹치는 부품을 단일 솔리드 IntersectOperation으로 교차합니다.

블록과 실린더.
개별 부품
하나의 객체로 결합된 블록과 실린더.
교차 결과

겹치는 부품을 교차하려면:

  1. 교차하려는 모든 부품을 선택합니다.
  2. 교차 버튼을 클릭합니다. 모든 부품이 하나의 솔리드 IntersectOperation으로 결합되며 이름은 교차입니다.

차감

차감 도구는 부품을 차감하여 다른 부품과 합쳐질 때 차감된 부품의 형태가 다른 부품에서 빼기 됩니다.

블록과 실린더.
블록과 차감된 실린더
하나의 객체로 결합된 블록과 실린더.
차감 결과

다른 겹치는 부품에서 부품을 빼려면:

  1. 다른 부품에서 빼고 싶은 부품을 선택합니다.
  2. 차감을 클릭합니다. 부품이 반투명해지며 상태를 나타내기 위해 붉은 색조가 추가됩니다.
  3. 차감된 부품과 차감할 부품을 모두 선택합니다.
  4. 합집합을 클릭합니다. 차감된 부품이 포함된 겹치는 부품에서 잘립니다.

분리

분리 도구는 UnionOperation을 개별 부품으로 다시 분리하여 본질적으로 합집합 및 교차에 대한 "실행 취소" 도구 역할을 합니다.

합집합 또는 교차를 개별 부품으로 다시 분리하려면:

  1. 합집합 또는 교차를 선택합니다.
  2. 분리를 클릭합니다. 부품이 원래 형태로 다시 분리됩니다.

게임 내 솔리드 모델링

게임이 실행되는 동안 또는 GeometryService 메서드를 통해 플러그인에서 솔리드 모델링 작업을 수행할 수도 있습니다.

기본 작업

스튜디오의 내장 작업 도구와 유사하게, GeometryService 메서드인 UnionAsync(), IntersectAsync(), SubtractAsync()를 사용하여 기본 불리언 작업을 수행할 수 있습니다. 예를 들어, 다음 스크립트는 SubtractAsync()를 사용하여 한 부품의 부피를 다른 부품에서 빼는 방법을 보여줍니다.

한 부품에서 다른 부품 빼기
local GeometryService = game:GetService("GeometryService")
local mainPart = Instance.new("Part")
local otherPart = Instance.new("Part")
otherPart.Position = Vector3.new(1, 0.5, 1)
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, {otherPart})
end)
if success and newParts then
for _, newPart in pairs(newParts) do
newPart.Parent = workspace
end
end

더욱 시연하기 위해, 다음 코드 샘플은 mainPart와 otherParts 배열의 기하학을 결합한 다음, 작업에 관련된 원래 부품을 파괴합니다. UnionAsync() 호출을 IntersectAsync() 또는 SubtractAsync()로 교체하여 다른 불리언 작업을 수행할 수 있습니다.

여러 부품의 기하학 결합
local GeometryService = game:GetService("GeometryService")
local mainPart = workspace.BlueBlock
local otherParts = { workspace.PurpleCylinder }
local options = {
CollisionFidelity = Enum.CollisionFidelity.Default,
RenderFidelity = Enum.RenderFidelity.Automatic,
SplitApart = false
}
local success, newParts = pcall(function()
return GeometryService:UnionAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- 결과 부품을 재부모화/재위치
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- 원래 부품 파괴
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end

부품 차감에 대한 엔진 수준의 메서드는 없지만, 스크립트나 플러그인 내에서 rbxNegate 태그를 추가하여 스튜디오의 차감 도구 모음 버튼과 동등한 차감을 수행할 수 있습니다.

스크립트를 통한 부품 차감
local CollectionService = game:GetService("CollectionService")
CollectionService:AddTag(workspace.Part, "rbxNegate")

BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync()와 비교하여 GeometryService 메서드는 다음과 같이 다릅니다:

  • 출력은 단일 인스턴스가 아닌 인스턴스 배열입니다.
  • 입력 부품은 장면에 부모로 설정할 필요가 없으며, 백그라운드 작업이 가능합니다.
  • SplitApart 옵션이 true(기본값)로 설정되면 각 개별 본체가 자신의 PartOperation/MeshPart로 반환됩니다.
  • 반환된 모든 부품은 주 부품의 좌표 공간에 있으므로, 그들의 PVInstance.Origin 위치는 주 부품과 동일합니다. 이는 메시의 정점이 작업 전과 동일한 위치에 상대적으로 유지되지만, 반환된 부품의 (0, 0, 0)이 본체의 중심에 반드시 위치하지는 않음을 의미합니다.

스윕 부품

SweepPartAsync() 메서드는 주어진 CFrame 위치 세트를 통해 드래그된 입력 부품의 형태를 가진 MeshPart를 생성합니다. 이 메서드는 자르기 및 절단 상호작용을 수행하는 데 매우 유용할 수 있습니다.

입력은 Part, PartOperation 또는 MeshPart일 수 있습니다. 결과의 형태는 각 인접한 CFrames 쌍의 볼록 껍질의 합집합으로 정의됩니다. 단일 CFrame만 제공되면 결과는 입력 부품의 볼록 껍질이 됩니다.

이 메서드가 어떻게 작동하는지 보여주기 위해, 다음 코드 샘플은 공을 일련의 CFrame 위치를 통해 스윕하여 나선형을 생성합니다:

나선형을 따라 스윕 부품
local GeometryService = game:GetService("GeometryService")
local inputPart = Instance.new("Part")
inputPart.Shape = Enum.PartType.Ball
local cframeList = {}
for i = 1, 50 do
local rotation = CFrame.Angles(0, i * 0.5, 0)
local position = Vector3.new(0, i * 0.1, -1)
table.insert(cframeList, rotation * CFrame.new(position))
end
local success, sweptPart = pcall(function()
return GeometryService:SweepPartAsync(inputPart, cframeList)
end)
if success and sweptPart then
sweptPart.Parent = workspace
end
구를 스윕하여 생성된 나선형 형태.
블록에서 차감된 반투명 곡선 형태.

이 예제는 SweepPartAsync()를 사용하여 플레이어의 마우스 위치를 기반으로 한 검이나 레이저 총 자르기 게임 플레이 기능을 구현합니다. 사용자의 마우스 움직임은 CFrames 목록으로 기록되며, SweepPartAsync()는 이 데이터를 기반으로 슬라이스 메시를 생성한 다음, 슬라이스 메시를 맞은 부품에서 차감합니다.

이 예제를 스튜디오에서 실행하려면:

  1. 모든 솔리드 모델링 작업을 수행하기 위해 ServerScriptService에 다음 Script를 생성합니다.

    local ReplicatedStorage = game:GetService("ReplicatedStorage")
    local GeometryService = game:GetService("GeometryService")
    local DrawCurveEvent = ReplicatedStorage:WaitForChild("DrawCurveEvent")
    DrawCurveEvent.OnServerEvent:Connect(function(player, cframeList, hitInstance)
    local blade = Instance.new("Part")
    blade.Size = Vector3.new(0.2, 0.2, 15.0)
    local success, sweptPart = pcall(function()
    return GeometryService:SweepPartAsync(blade, cframeList)
    end)
    if success and sweptPart then
    -- 스윕 시각화
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- 맞은 인스턴스에서 스윕 차감
    local subtractSuccess, newParts = pcall(function()
    return GeometryService:SubtractAsync(hitInstance, {sweptPart})
    end)
    if subtractSuccess and newParts then
    for _, newPart in pairs(newParts) do
    newPart.Parent = hitInstance.Parent
    newPart.Anchored = true
    end
    hitInstance:Destroy()
    end
    end
    end)
  2. 사용자 입력을 처리하기 위해 StarterPlayerScripts에 다음 LocalScript를 생성합니다.

    local ReplicatedStorage = game:GetService("ReplicatedStorage")
    local GeometryService = game:GetService("GeometryService")
    local DrawCurveEvent = ReplicatedStorage:WaitForChild("DrawCurveEvent")
    DrawCurveEvent.OnServerEvent:Connect(function(player, cframeList, hitInstance)
    local blade = Instance.new("Part")
    blade.Size = Vector3.new(0.2, 0.2, 15.0)
    local success, sweptPart = pcall(function()
    return GeometryService:SweepPartAsync(blade, cframeList)
    end)
    if success and sweptPart then
    -- 스윕 시각화
    sweptPart.Parent = workspace
    sweptPart.Transparency = 0.5
    sweptPart.Anchored = true
    sweptPart.CanQuery = false
    -- 맞은 인스턴스에서 스윕 차감
    local subtractSuccess, newParts = pcall(function()
    return GeometryService:SubtractAsync(hitInstance, {sweptPart})
    end)
    if subtractSuccess and newParts then
    for _, newPart in pairs(newParts) do
    newPart.Parent = hitInstance.Parent
    newPart.Anchored = true
    end
    hitInstance:Destroy()
    end
    end
    end)
  3. ReplicatedStorage에 DrawCurveEvent라는 RemoteEvent를 생성합니다.

파편화

FragmentAsync() 및 GenerateFragmentSites() 메서드는 부품을 자연스러운 형태의 조각으로 부수는 기능을 제공합니다. FragmentAsync()는 전달된 점 패턴에 따라 단일 부품을 여러 MeshPart 인스턴스로 나누기 위해 보로노이 분해를 사용하며, GenerateFragmentSites()는 FragmentAsync()에 전달할 보로노이 사이트로 알려진 점을 생성하는 도우미 메서드입니다.

다음 코드 샘플은 블록 부품을 파편화하기 위해 보로노이 사이트를 생성합니다:

local GeometryService = game:GetService("GeometryService")
local inputPart = Instance.new("Part")
inputPart.Position = Vector3.new(0, 0.7, 20)
local sites = GeometryService:GenerateFragmentSites(inputPart)
local success, fragments = pcall(function()
return GeometryService:FragmentAsync(inputPart, sites)
end)
if success and fragments then
for _, item in fragments do
local instance = item.Instance
instance.Parent = workspace
end
end
조각으로 부서진 블록.
모서리가 조각난 블록.

다음 스크립트는 위치와 반경에 따라 부품의 영역을 파편화합니다. 위치는 일반적으로 물리적 충돌이나 플레이어의 레이캐스트에서 올 수 있습니다.

GenerateFragmentSites()가 생성하는 사이트 배열의 첫 번째 요소는 요청된 radius 밖에 있는 모든 사이트의 내부 배열이 됩니다. 부품의 '파손되지 않은 부분'에 대해 특정 작업을 수행하려면, FragmentAsync()의 결과를 반복할 때 fragments[i].Index == 1을 확인하여 해당 부분을 찾을 수 있습니다.

local GeometryService = game:GetService("GeometryService")
local function fragmentAtPosition(player, part, contactPoint, radius)
local allSites = GeometryService:GenerateFragmentSites(part, {Origin = contactPoint, Radius = radius})
local success, fragments = pcall(function()
return GeometryService:FragmentAsync(part, allSites)
end)
if not success then
warn("파손 실패:"..tostring(fragments))
return
end
local decals = {}
for _,child in pairs(part:GetChildren()) do
if child:IsA("Decal") or child:IsA("SurfaceAppearance") then
table.insert(decals,child)
end
end
for i = 1, #fragments do
local fragment = fragments[i].Instance
if fragment == nil then
continue
end
for _,d in pairs(decals) do
local d2 = d:Clone()
d2.Parent = fragment
end
fragment.Anchored = false
fragment.Parent = part.Parent
fragment:SetNetworkOwner(player)
end
if #fragments ~= 0 then
part:Destroy()
end
return fragments
end

다음 스크립트는 두 번째 부품을 스텐실로 사용하여 첫 번째 부품에서 조각을 부수는 기능을 제공합니다. 두 번째 부품 내의 보로노이 사이트만이 개별 조각으로 결과를 생성합니다. 다른 모든 사이트는 그들의 셀을 단일 부품으로 결합합니다.

Roblox Studio 로고 모양의 스텐실
부품(어두운 회색)과 스텐실 부품
스텐실 모양으로 파편화된 결과
파편화 스크립트 결과
local GeometryService = game:GetService("GeometryService")
local function fragmentWithinStencil(player, part)
local overlapParams = OverlapParams.new()
overlapParams.FilterType = Enum.RaycastFilterType.Include
overlapParams.FilterDescendantsInstances = {workspace.Stencil}
overlapParams.RespectCanCollide = false
local sensor = Instance.new("Part")
sensor.Size = Vector3.new(0.01, 0.01, 0.01)
local allSites = GeometryService:GenerateFragmentSites(part, {SiteSpacing = 0.9})
local fragmentSites = {}
local mainPartSites = {}
for _, site in ipairs(allSites) do
sensor.CFrame = CFrame.new(site)
local partsFound = workspace:GetPartsInPart(sensor, overlapParams)
if #partsFound > 0 then
table.insert(fragmentSites, site)
else
table.insert(mainPartSites, site)
end
end
local sortedSites = fragmentSites
table.insert(sortedSites, mainPartSites)
workspace.Stencil:Destroy()
local success, fragments = pcall(function()
return GeometryService:FragmentAsync(part, sortedSites, {SplitApart = false})
end)
if not success then
warn("파손 실패:"..tostring(fragments))
return
end
local decals = {}
for _,child in pairs(part:GetChildren()) do
if child:IsA("Decal") or child:IsA("SurfaceAppearance") then
table.insert(decals,child)
end
end
for i = 1, #fragments do
local fragment = fragments[i].Instance
if fragment == nil then
continue
end
for _,d in pairs(decals) do
local d2 = d:Clone()
d2.Parent = fragment
end
fragment.Anchored = false
fragment.Parent = part.Parent
fragment:SetNetworkOwner(player)
end
if #fragments ~= 0 then
part:Destroy()
end
return fragments
end

다음 스크립트는 훨씬 더 특수한 사용 사례이지만, FragmentAsync()에서 반환된 인덱스 데이터를 활용하는 방법을 보여줍니다.

예를 들어, 많은 장소에는 여러 비연합 블록 부품으로 형성된 건물이 있습니다. 수류탄, 포탄 또는 쇠망치가 이를 손상시키면 모든 벽 부품이 파편화되기를 원할 것입니다. 이 스크립트는 근처의 모든 부품을 파편화한 다음, 서로 다른 부품의 파편을 결합하여 이음새를 완전히 숨깁니다.

이 작업은 여러 Async() 작업을 포함하므로, 즉각적인 사용자 입력에 대한 응답으로 게임 내에서 사용하기에는 적합하지 않을 수 있습니다.

블록의 행
블록의 행
파편화된 블록의 행
각 조각은 여러 입력 부품에서 유래할 수 있습니다
local GeometryService = game:GetService("GeometryService")
local function fragmentCrossPart(player, part, contactPoint, radius)
local allSites = GeometryService:GenerateFragmentSites(part, {Origin = contactPoint, Radius = radius})
local fragmentsSorted = {}
for i = 1, #allSites do
fragmentsSorted[i] = {}
end
local partsFound = workspace:GetPartBoundsInRadius(contactPoint, radius)
for i, part in ipairs(partsFound) do
local success, fragments = pcall(function()
return GeometryService:FragmentAsync(part, allSites)
end)
if not success then
warn("파손 실패:"..tostring(fragments))
return
end
for i = 1, #fragments do
local fragment = fragments[i].Instance
local siteIndex = fragments[i].Index
if fragment == nil or siteIndex == nil then
continue
end
table.insert(fragmentsSorted[siteIndex], fragment)
end
end
for i = 1, #fragmentsSorted do
local fragmentList = fragmentsSorted[i]
if #fragmentList == 0 then
continue
end
if #fragmentList == 1 then
local fragment = fragmentList[1]
fragment.Anchored = false
fragment.Parent = part.Parent
fragment:SetNetworkOwner(player)
continue
end
if i == #allSites then
for j = 1, #fragmentList do
local fragment = fragmentList[j]
fragment.Parent = part.Parent
fragment.Anchored = true
end
continue
end
local mainPart = fragmentList[1]
local otherParts = {}
for j = 2, #fragmentList do
table.insert(otherParts, fragmentList[j])
end
local success, results = pcall(function()
return GeometryService:UnionAsync(mainPart, otherParts)
end)
if not success then
warn("합치기 실패:"..tostring(results))
return
end
for j = 1, #results do
results[j].Parent = part.Parent
results[j].Anchored = false
results[j]:SetNetworkOwner(player)
end
end
for i, part in ipairs(partsFound) do
part:Destroy()
end
end

다음 스크립트는 GenerateFragmentSites()의 거의 동일한 Luau 대체물입니다. GenerateFragmentSites()와 유사한 동작을 원하지만 약간의 변경을 원한다면, 이를 시작점으로 사용할 수 있습니다.

이 스크립트는 점의 지터링 그리드를 사용하며, 완전히 무작위 점과는 달리 파편화된 영역이 잘 작동하도록 보장합니다.

local function generateFragmentSites(part: BasePart, siteSpacing: number?, origin: Vector3?, radius: number?): {Vector3}
local RANDOMNESS_MULTIPLIER = 1.0 -- 지터의 양을 조정하는 데 사용
if (origin and not radius) or (radius and not origin) then
warn("원점과 반경을 모두 제공하거나 둘 다 제공하지 않아야 합니다.")
return {}
end
local isLocalized = (radius ~= nil) -- isLocalized는 전체 부품을 파편화하지 않고 섹션만 파편화합니다.
local partCFrame = part.ExtentsCFrame
local gridDimensions: Vector3
local localGridCenter: Vector3
local spacing
if siteSpacing then
spacing = siteSpacing
elseif isLocalized then
spacing = radius * 0.5
else
local partSize = part.Size
local volume = partSize.X * partSize.Y * partSize.Z
spacing = (volume / 5) ^ (1/3)
end
if isLocalized then
local localOrigin = partCFrame:PointToObjectSpace(origin)
local gridSize = math.ceil(radius * 2 / spacing) + 3
gridDimensions = Vector3.new(gridSize, gridSize, gridSize)
localGridCenter = localOrigin
else
local partSize: Vector3 = part.Size
local xCount = math.ceil(partSize.X / spacing)
local yCount = math.ceil(partSize.Y / spacing)
local zCount = math.ceil(partSize.Z / spacing)
gridDimensions = Vector3.new(xCount, yCount, zCount)
localGridCenter = Vector3.zero
end
local totalGridSize = gridDimensions * spacing
local halfCell = Vector3.new(spacing, spacing, spacing) * 0.5
local localStartOffset = localGridCenter - (totalGridSize * 0.5) + halfCell
local innerJitter = spacing * 0.5 * RANDOMNESS_MULTIPLIER
local outerJitter = math.min(spacing * 0.5 * 0.866, innerJitter)
local sitesFlatList = {}
for x = 0, gridDimensions.X - 1 do
for y = 0, gridDimensions.Y - 1 do
for z = 0, gridDimensions.Z - 1 do
local isOuterShell =
x == 0 or x == gridDimensions.X - 1 or
y == 0 or y == gridDimensions.Y - 1 or
z == 0 or z == gridDimensions.Z - 1
local jitterAmount = if (isOuterShell and isLocalized) then outerJitter else innerJitter
local jitterOffset = Vector3.new(
(math.random() - 0.5) * 2 * jitterAmount,
(math.random() - 0.5) * 2 * jitterAmount,
(math.random() - 0.5) * 2 * jitterAmount
)
local offsetInGrid = Vector3.new(x, y, z) * spacing
table.insert(sitesFlatList, localStartOffset + offsetInGrid + jitterOffset)
end
end
end
local sitesListFinal = {}
if isLocalized then
local mainPartSites = {}
for _, localSite in ipairs(sitesFlatList) do
local worldSite = partCFrame * localSite
local distance = (worldSite - origin).Magnitude
if distance < radius then
table.insert(sitesListFinal, worldSite)
else
table.insert(mainPartSites, worldSite)
end
end
table.insert(sitesListFinal, 1, mainPartSites)
else
for _, localSite in ipairs(sitesFlatList) do
local worldSite = partCFrame * localSite
table.insert(sitesListFinal, worldSite)
end
end
return sitesListFinal
end

제약 조건 유지

입력 부품에 유지하려는 제약 조건이나 부착물이 있는 경우, 결과 부품으로 전송할 수 있습니다. 어떤 출력 부품에 제약 조건을 부착해야 할지 파악하는 것은 번거로울 수 있으므로, CalculateConstraintsToPreserve()를 사용하여 추천 테이블을 생성한 다음 이를 반복하여 적용하는 것이 좋습니다.

다음 코드 샘플은 차감 작업을 수행하고, 결과 부품을 재부모화 및 재위치한 다음, 원래 부품을 파괴하기 전에 유지하거나 삭제할 제약 조건 및 부착물의 테이블을 계산합니다.

local GeometryService = game:GetService("GeometryService")
local mainPart = workspace.PurpleBlock
local otherParts = { workspace.BlueBlock }
local options = {
CollisionFidelity = Enum.CollisionFidelity.Default,
RenderFidelity = Enum.RenderFidelity.Automatic,
SplitApart = true,
}
local constraintOptions = {
tolerance = 0.1,
weldConstraintPreserve = Enum.WeldConstraintPreserve.All,
dropAttachmentsWithoutConstraints = false,
}
-- 비동기적이므로 pcall() 내에서 차감 작업 수행
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- 결과 부품을 재부모화/재위치
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- 유지하거나 삭제할 제약 조건/부착물 계산
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- 추천 테이블에 따라 제약 조건/부착물 유지
for _, item in pairs(recommendedTable) do
if item.Attachment then
item.Attachment.Parent = item.AttachmentParent
if item.Constraint then
item.Constraint.Parent = item.ConstraintParent
end
elseif item.NoCollisionConstraint then
local newNoCollision = Instance.new("NoCollisionConstraint")
newNoCollision.Part0 = item.NoCollisionPart0
newNoCollision.Part1 = item.NoCollisionPart1
newNoCollision.Parent = item.NoCollisionParent
elseif item.WeldConstraint then
local newWeldConstraint = Instance.new("WeldConstraint")
newWeldConstraint.Part0 = item.WeldConstraintPart0
newWeldConstraint.Part1 = item.WeldConstraintPart1
newWeldConstraint.Parent = item.WeldConstraintParent
end
end
-- 원래 부품 파괴
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end

솔리드 모델링 결과

동작 세부정보

  • 원래 부품은 작업이 성공적으로 수행된 후에도 그대로 유지되며, 반환된 부품은 설정된 Parent가 없습니다. 대부분의 경우, 반환된 부품을 주 부품과 동일한 위치에 부모로 설정한 다음, 원래 부품을 Destroy()하는 것이 좋습니다.

  • 주 부품이 작업 계산 중에 이동 중인 경우, 반환된 부품을 주 부품의 업데이트된 CFrame으로 설정할 수 있습니다. 반환된 부품은 주 부품과 동일한 좌표 공간에 있기 때문입니다.

  • 솔리드 모델링 작업이 20,000개 이상의 삼각형을 가진 부품을 생성하면, 이들은 20,000으로 단순화됩니다. 이를 수행할 수 없는 경우, 일반적으로 수천 개의 비겹치는 구성 요소가 있는 경우 작업이 오류로 끝납니다.

  • 인스턴스의 메시 데이터를 교체하는 방법이 있으며, 이를 통해 주 부품의 속성, 속성, 태그 및 자식을 유지하면서 작업의 기하학을 활용할 수 있습니다. 이 접근 방식은 원래 PartOperation을 다른 것으로 완전히 교체할 때 발생할 수 있는 "깜박임"을 피할 수 있습니다.

  • 클라이언트에서 이러한 메서드를 호출하는 것이 가능하지만 몇 가지 제한이 있습니다. 첫째, 클라이언트에서 생성된 객체로 수행해야 합니다. 둘째, 클라이언트에서 서버로의 복제는 제공되지 않습니다.

색상 및 UV

솔리드 모델링 후 결과 부품의 색상은 두 곳에서 옵니다: 면 색상과 부품의 Color.

  • 결과가 PartOperation인 경우, 스튜디오에서 선택한 첫 번째 부품의 Color를 가지지만, 스튜디오는 기본적으로 각 면이 작업 전과 동일한 색상을 유지하도록 면 색상을 사용합니다. 이 동작을 무시하고 전체 결과를 단일 색상으로 만들려면 스튜디오에서 UsePartColor 속성을 활성화할 수 있습니다.
  • 결과가 MeshPart인 경우, Color는 흰색이 되며, 면 색상이 항상 보입니다. 결과 부품의 색조를 조정하려면 Color를 변경하면 되지만, 이는 면 색상과 혼합(곱해짐)됩니다. 결과를 완전히 제어하려면 입력을 먼저 흰색으로 만드는 것이 가장 좋습니다.

UV는 결과 유형에 따라 다르게 처리됩니다:

  • PartOperations는 항상 박스 매핑된 UV를 가지며, 이는 각 면이 한 방향( -x, +x, -y, +y, -z, +z 중 하나)에서 재료/텍스처/데칼이 적용됨을 의미합니다. 이는 텍스처를 늘릴 수 있습니다.
  • MeshParts는 박스 매핑되지 않습니다. 주 부품의 메시의 UV가 사용됩니다. Roblox는 현재 다중 재료 지원이 없으므로, 다른 부품에서 유래한 면의 UV는 (0, 0)의 UV를 가집니다. 최상의 결과를 얻으려면 텍스처의 픽셀 (0, 0)이 합리적인 색상을 가지도록 해야 합니다.

스무딩 각도

솔리드 모델링된 부품의 SmoothingAngle 속성은 동일한 색상의 인접한 표면 간의 각도를 부드럽게 합니다. 더 높은 값은 더 부드러운 외관을 생성하고, 더 낮은 값은 더 날카로운 모서리로 더 거친 외관을 생성합니다.

30도에서 70도 사이의 값은 일반적으로 좋은 결과를 생성하지만, 90도에서 180도 사이의 값은 날카로운 모서리의 합집합 및 교차에서 "그림자 효과"를 유발할 수 있으므로 권장되지 않습니다.

©2026 Roblox Corporation. Roblox 및 Roblox 로고, 'Powering Imagination'은 미국 및 기타 국가 내 당사의 등록 및 미등록 상표입니다.