การสร้างโมเดลแบบแข็ง คือกระบวนการในการรวม BaseParts เข้าด้วยกันในรูปแบบที่ไม่ซ้ำกันเพื่อสร้างรูปร่างที่ซับซ้อนมากขึ้น ซึ่งรวมถึงการดำเนินการแบบบูลีน การรวม, การตัดกัน, และ การลบ ซึ่งเป็นที่รู้จักกันทั่วไปว่าเป็นเรขาคณิตเชิงสร้างสรรค์ (CSG) คุณสามารถทำการสร้างโมเดลแบบแข็งใน Studio, ปลั๊กอิน, และ ในเกม ทั้งในเซิร์ฟเวอร์และไคลเอนต์
นอกจาก CSG แบบบูลีนแล้ว การสร้างโมเดลแบบแข็งยังรองรับเมช ตราบใดที่มัน ปิดสนิท และการดำเนินการเช่น การกวาด และ การแตก ที่ช่วยให้คุณและผู้เล่นของคุณสามารถตัด, หั่น, และทำให้เรขาคณิตแตกออกเพื่อสร้างปฏิสัมพันธ์ในการเล่นเกมที่ไม่เหมือนใคร


เรขาคณิตที่ปิดสนิท
การดำเนินการสร้างโมเดลแบบแข็งสามารถทำงานได้เฉพาะกับเรขาคณิตที่ ปิดสนิท ในทางเทคนิค เมชที่ปิดสนิทหมายความว่ามันถูกปิด, มีหลายมิติ, และไม่ตัดกันเอง คำเหล่านี้มีการกำหนดที่เข้มงวด แต่มีบางกฎง่ายๆ ดังนี้:
- แต่ละ หน้า ต้องมีด้าน "ภายใน" และด้าน "ภายนอก" ซึ่งถูกกำหนดโดยลำดับการหมุนของหน้า ซึ่งเป็นลำดับของสามจุดยอดของมัน
- แต่ละ ขอบ ต้องถูกแชร์โดยสามเหลี่ยมสองตัวเท่านั้น ซึ่งหมายความว่าไม่สามารถมีรูในเมชได้เพราะขอบรอบขอบของรูจะมีเพียงสามเหลี่ยมหนึ่งตัว
- หน้าไม่สามารถผ่านหน้าอื่นได้
- สามเหลี่ยมที่อยู่ติดกันต้องเห็นพ้องกันว่าด้านไหนคือด้าน "ภายนอก"
- แต่ละ จุดยอด ต้องมีพัดลมของสามเหลี่ยมที่อยู่ติดกันเพียงหนึ่งตัวเท่านั้น
แต่ละตัวอย่างต่อไปนี้เป็น ไม่ปิดสนิท สำหรับเหตุผลที่ระบุไว้:




ระบบการสร้างโมเดลแบบแข็งสามารถซ่อมแซมปัญหาขนาดเล็กบางอย่างกับเมชโดยอัตโนมัติ แต่โดยทั่วไปแล้ว การเรียก API จะล้มเหลวหากเมชไม่ปิดสนิท ไม่มีวิธีการซ่อมแซมที่เหมาะสมสำหรับเมชที่ไม่ปิดสนิทที่มีอยู่ แต่มีปลั๊กอิน Blender หลายตัวที่สามารถช่วยได้ เช่น 3D Print Toolbox และ Mesh Repair Tools นอกจากนี้ Meshlab ยังมีเครื่องมือที่มีประโยชน์มากมายในตัวเพื่อพยายามทำให้เมชมีหลายมิติ ซึ่งเป็นข้อกำหนดหลักสำหรับเมชที่ปิดสนิท
วิธีหนึ่งในการดูว่าเมชจะยากมากที่จะทำให้ปิดสนิทคือการดูจาก Studio จากทุกมุม แล้วลองเปิดและปิดคุณสมบัติ MeshPart.DoubleSided ของเมช หากคุณเห็นความแตกต่างใดๆ แสดงว่าเมชเป็นเพียงเปลือกและเครื่องมือที่กล่าวถึงข้างต้นจะไม่ทำงานเพราะไม่สามารถเดาได้ว่าสิ่งที่อยู่ภายในและภายนอกเมชคืออะไร อย่างไรก็ตาม หากสิ่งที่คุณต้องการคือเมชที่บางและไม่สำคัญที่จะรักษาขนาดของเมชให้เหมือนเดิม คุณสามารถใช้ Solidify modifier ของ Blender เพื่อทำให้เปลือกหนาขึ้นเล็กน้อยจนกลายเป็นเมชที่ปิดสนิท


การสร้างโมเดลแบบแข็งใน Studio
คุณสามารถทำการดำเนินการบูลีนพื้นฐานสามอย่างโดยใช้เครื่องมือสี่ตัวภายในแถบเครื่องมือ โมเดล

| เครื่องมือ | ทางลัด | คำอธิบาย |
|---|---|---|
| การรวม | ShiftCtrlG (Windows) Shift⌘G (Mac) | รวมชิ้นส่วนสองชิ้นหรือมากกว่ากันเพื่อสร้างการรวมที่เป็นของแข็งเดียว |
| การตัดกัน | ShiftCtrlI (Windows) Shift⌘I (Mac) | ตัดกันชิ้นส่วนที่ทับซ้อนกันให้เป็นการตัดกันที่เป็นของแข็งเดียว |
| แยก | ShiftCtrlU (Windows) Shift⌘U (Mac) | แยกการรวม หรือการตัดกันกลับไปเป็นชิ้นส่วนเดิม |
| การลบ | ShiftCtrlN (Windows) Shift⌘N (Mac) | ลบชิ้นส่วน ซึ่งมีประโยชน์สำหรับการสร้างรูและรอยบุ๋ม |
การรวม
เครื่องมือ การรวม จะรวมชิ้นส่วนสองชิ้นหรือมากกว่ากันเพื่อสร้าง UnionOperation ที่เป็นของแข็งเดียว


ในการรวมชิ้นส่วนเข้าด้วยกัน:
- เลือกชิ้นส่วนทั้งหมดที่คุณต้องการรวมเข้าด้วยกัน
การตัดกัน
เครื่องมือ การตัดกัน จะตัดกันชิ้นส่วนที่ทับซ้อนกันให้เป็น IntersectOperation ที่เป็นของแข็งเดียว


ในการตัดกันชิ้นส่วนที่ทับซ้อนกัน:
- เลือกชิ้นส่วนทั้งหมดที่คุณต้องการตัดกัน
- คลิกปุ่ม การตัดกัน ชิ้นส่วนทั้งหมดจะรวมกันเป็น IntersectOperation ที่เป็นของแข็งเดียวโดยมีชื่อว่า การตัดกัน
การลบ
เครื่องมือ การลบ จะลบชิ้นส่วนหนึ่งชิ้นเพื่อให้เมื่อมันถูก รวมกับชิ้นส่วนอื่น รูปร่างของชิ้นส่วนที่ถูกลบจะ ถูกลบ ออกจากชิ้นส่วนอื่น


ในการลบชิ้นส่วนจากชิ้นส่วนที่ทับซ้อนกันอื่น:
- เลือกชิ้นส่วนที่คุณต้องการลบจากชิ้นส่วนอื่น
- คลิก การลบ ชิ้นส่วนจะกลายเป็นโปร่งใสด้วยสีแดงเพื่อบ่งบอกสถานะของมัน
- เลือกทั้งชิ้นส่วนที่ถูกลบและชิ้นส่วนที่คุณต้องการลบออกจากมัน
- คลิก การรวม ชิ้นส่วนที่ถูกลบจะถูกตัดออกจากชิ้นส่วนที่ทับซ้อนกันที่รวมอยู่
แยก
เครื่องมือ แยก จะทำการแยก UnionOperation กลับไปเป็นชิ้นส่วนเดิม ซึ่งทำหน้าที่เป็นเครื่องมือ "เลิกทำ" สำหรับการรวมและการตัดกัน
ในการแยกการรวม หรือการตัดกันกลับไปเป็นชิ้นส่วนเดิม:
- เลือกการรวม หรือการตัดกัน
- คลิก แยก ชิ้นส่วนจะกลับไปเป็นรูปแบบเดิม
การสร้างโมเดลแบบแข็งในเกม
คุณยังสามารถทำการดำเนินการสร้างโมเดลแบบแข็งในขณะที่เกมกำลังทำงานหรือจาก ปลั๊กอิน ผ่านวิธี GeometryService
การดำเนินการหลัก
คล้ายกับเครื่องมือการดำเนินการใน Studio คุณสามารถใช้วิธี 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 ภายในสคริปต์หรือ ปลั๊กอิน เพื่อทำการลบที่เทียบเท่ากับปุ่มในแถบเครื่องมือ การลบ ของ Studio
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() จะสร้าง MeshPart ที่มีรูปร่างของชิ้นส่วนที่ป้อนเข้ามาซึ่งถูกลากผ่านชุดตำแหน่ง CFrame ที่กำหนด วิธีนี้สามารถมีประโยชน์มากสำหรับการทำการตัดและการหั่น
การป้อนข้อมูลสามารถเป็น 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() จะสร้างเมชการตัดจากข้อมูลนี้ จากนั้นเมชการตัดจะถูกลบออกจากชิ้นส่วนที่ถูกตี
เพื่อให้ตัวอย่างนี้ทำงานใน Studio:
สร้าง Script ต่อไปนี้ใน ServerScriptService เพื่อทำการดำเนินการสร้างโมเดลแบบแข็งทั้งหมด
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 = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- ลบการกวาดออกจากอินสแตนซ์ที่ถูกตีlocal subtractSuccess, newParts = pcall(function()return GeometryService:SubtractAsync(hitInstance, {sweptPart})end)if subtractSuccess and newParts thenfor _, newPart in pairs(newParts) donewPart.Parent = hitInstance.ParentnewPart.Anchored = trueendhitInstance:Destroy()endendend)สร้าง LocalScript ต่อไปนี้ใน StarterPlayerScripts เพื่อจัดการกับการป้อนข้อมูลของผู้ใช้
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 = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- ลบการกวาดออกจากอินสแตนซ์ที่ถูกตีlocal subtractSuccess, newParts = pcall(function()return GeometryService:SubtractAsync(hitInstance, {sweptPart})end)if subtractSuccess and newParts thenfor _, newPart in pairs(newParts) donewPart.Parent = hitInstance.ParentnewPart.Anchored = trueendhitInstance:Destroy()endendend)สร้าง RemoteEvent ใน ReplicatedStorage ชื่อ DrawCurveEvent
การแตก
วิธี FragmentAsync() และ GenerateFragmentSites() ช่วยให้คุณสามารถทำให้ชิ้นส่วนแตกออกเป็นชิ้นเล็กๆ ที่มีรูปร่างดูเป็นธรรมชาติ FragmentAsync() ใช้การแยกแบบ voronoi เพื่อแบ่งชิ้นส่วนเดียวออกเป็นหลาย MeshPart ตามรูปแบบของจุดที่ถูกส่งเข้าไป ในขณะที่ GenerateFragmentSites() เป็นวิธีช่วยที่สร้างจุดที่เรียกว่าไซต์ voronoi เพื่อส่งเข้าไปใน FragmentAsync()
ตัวอย่างโค้ดต่อไปนี้สร้างไซต์ voronoi เพื่อทำให้บล็อกชิ้นส่วนแตกออก:
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() สร้างขึ้นจะเป็นอาร์เรย์ภายในของไซต์ทั้งหมดที่อยู่นอก รัศมี ที่ร้องขอ หากคุณต้องการทำบางอย่างเฉพาะกับ 'ส่วนที่ไม่แตก' ที่เหลือของชิ้นส่วน คุณสามารถหาส่วนที่เหลือนั้นได้โดยการตรวจสอบ fragments[i].Index == 1 เมื่อวนรอบผลลัพธ์ของ FragmentAsync()
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สคริปต์ต่อไปนี้จะแตกชิ้นส่วนออกจากชิ้นส่วนแรกภายในรูปร่างที่กำหนดโดยการใช้ชิ้นส่วนที่สองเป็นแม่แบบ ไซต์ voronoi เท่านั้นที่อยู่ภายในชิ้นส่วนที่สองจะส่งผลให้เกิดชิ้นส่วนแยกออกมา ชิ้นส่วนอื่นๆ จะมีเซลล์รวมกันเป็นชิ้นส่วนเดียว


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สคริปต์ต่อไปนี้เป็นการแทนที่ Luau ที่เกือบจะเหมือนกันสำหรับ GenerateFragmentSites() หากคุณต้องการพฤติกรรมที่คล้ายคลึงกับ 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ผลลัพธ์การสร้างโมเดลแบบแข็ง
รายละเอียดพฤติกรรม
หากชิ้นส่วนหลักกำลังเคลื่อนที่ในระหว่างการคำนวณการดำเนินการ คุณสามารถตั้งค่าชิ้นส่วนที่ได้ผลลัพธ์ให้เป็น CFrame ที่อัปเดตของชิ้นส่วนหลัก เนื่องจากชิ้นส่วนที่ส่งกลับอยู่ในพื้นที่พิกัดเดียวกันกับชิ้นส่วนหลัก
หากการดำเนินการสร้างโมเดลแบบแข็งจะส่งผลให้มีชิ้นส่วนใดๆ ที่มีมากกว่า 20,000 สามเหลี่ยม พวกมันจะถูกทำให้เรียบง่ายลงเหลือ 20,000 หากไม่สามารถทำได้ โดยปกติในกรณีที่มีส่วนประกอบที่ไม่ทับซ้อนกันหลายพันชิ้น การดำเนินการจะส่งผลให้เกิดข้อผิดพลาด
มีวิธีการในการเปลี่ยนข้อมูลเมชของอินสแตนซ์ ทำให้ใช้งานเรขาคณิตของการดำเนินการได้ง่ายขึ้นในขณะที่รักษาคุณสมบัติ, แอตทริบิวต์, แท็ก, และลูกของชิ้นส่วนหลัก เช่น Attachments, Constraints, ParticleEmitters, วัตถุแสง, และสติ๊กเกอร์ วิธีนี้ยังหลีกเลี่ยง "การกระพริบ" ที่อาจเกิดขึ้นจากการแทนที่ PartOperation เดิมด้วยอีกอันหนึ่ง
- หากใช้วิธีนี้กับ PartOperation เป็นชิ้นส่วนหลักและไม่มีชิ้นส่วนอื่นๆ เป็น MeshParts คุณสามารถแทนที่เรขาคณิตของ PartOperation อื่นได้ผ่าน SubstituteGeometry()
- หากชิ้นส่วนหลักเป็น MeshPart คุณสามารถใช้ MeshPart:ApplyMesh()
สามารถเรียกใช้วิธีเหล่านี้จากไคลเอนต์ได้ แต่มีข้อจำกัดบางประการ ประการแรก ต้องทำกับวัตถุที่ สร้าง บนไคลเอนต์ ประการที่สอง ไม่มีการทำซ้ำจากไคลเอนต์ไปยังเซิร์ฟเวอร์
คุณสมบัติต่อไปนี้จากชิ้นส่วนหลักจะถูกนำไปใช้กับ PartOperations หรือ MeshParts ที่ได้ผลลัพธ์:
สีและ UVs
สีของชิ้นส่วนที่ได้ผลลัพธ์หลังจากการสร้างโมเดลแบบแข็งมาจากสองที่: สีของหน้าและ Color ของชิ้นส่วน
- หากผลลัพธ์เป็น PartOperation มันจะมี Color ของชิ้นส่วนแรกที่คุณเลือกใน Studio แต่ Studio ใช้สีของหน้าเป็นค่าเริ่มต้นเพื่อให้แต่ละหน้ามีสีเดียวกันกับที่เป็นอยู่ก่อนการดำเนินการ คุณสามารถเปิดใช้งานคุณสมบัติ UsePartColor ใน Studio เพื่อแทนที่พฤติกรรมนี้และทำให้ผลลัพธ์ทั้งหมดเป็นสีเดียว
- หากผลลัพธ์เป็น MeshPart สีของมันจะเป็นสีขาว และสีของหน้าจะถูกแสดงเสมอ คุณสามารถปรับสีของชิ้นส่วนที่ได้ผลลัพธ์โดยการเปลี่ยน Color ของพวกมัน แต่จะถูกผสม (คูณ) กับสีของหน้า สิ่งนี้จะทำให้ผลลัพธ์มีสีที่ปรับแต่งแทนที่จะทำให้สีของหน้าหายไปโดยสิ้นเชิง หากคุณต้องการควบคุมสีของผลลัพธ์อย่างสมบูรณ์ ควรทำให้ชิ้นส่วนที่ป้อนเข้ามาเป็นสีขาวก่อน
UVs ยังถูกจัดการแตกต่างกันขึ้นอยู่กับประเภทของผลลัพธ์:
- PartOperations จะมี UV ที่ถูกกล่องแมพเสมอ ซึ่งหมายความว่าทุกหน้าจะมีวัสดุ/พื้นผิว/สติ๊กเกอร์จากทิศทางหนึ่ง (หนึ่งใน -x, +x, -y, +y, -z, +z) ถูกนำไปใช้กับมัน ซึ่งอาจทำให้พื้นผิวยืด
- MeshParts จะไม่ถูกกล่องแมพ UV ของเมชของชิ้นส่วนหลักจะถูกนำมาใช้ เนื่องจาก Roblox ยังไม่มีการสนับสนุนหลายวัสดุ UV ของหน้าที่มาจากชิ้นส่วนอื่นจะถูกกำหนด UV เป็น (0, 0) เพื่อผลลัพธ์ที่ดีที่สุด ควรตรวจสอบให้แน่ใจว่าพิกเซล (0, 0) ของพื้นผิวของคุณมีสีที่เหมาะสม
มุมการทำให้เรียบ
คุณสมบัติ SmoothingAngle ของชิ้นส่วนที่สร้างโมเดลแบบแข็งจะทำให้มุมระหว่างพื้นผิวที่อยู่ติดกันของสีเดียวกันเรียบขึ้น ค่าที่สูงกว่าจะทำให้ดูเรียบขึ้นในขณะที่ค่าที่ต่ำกว่าจะทำให้ดูหยาบขึ้นด้วยขอบที่คมชัดมากขึ้น
ในขณะที่ค่าระหว่าง 30 ถึง 70 องศามักจะให้ผลลัพธ์ที่ดี ค่าระหว่าง 90 ถึง 180 ไม่แนะนำเนื่องจากอาจทำให้เกิด "เงา" บนการรวมและการตัดกันที่มีขอบคม

