การสร้างโมเดลแบบแข็ง

*เนื้อหานี้แปลโดยใช้ AI (เวอร์ชัน Beta) และอาจมีข้อผิดพลาด หากต้องการดูหน้านี้เป็นภาษาอังกฤษ ให้คลิกที่นี่

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

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

MeshPart ที่สร้างขึ้นด้วย SweepPartAsync()
MeshPart ที่แตกด้วย FragmentAsync()

เรขาคณิตที่ปิดสนิท

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

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

แต่ละตัวอย่างต่อไปนี้เป็น ไม่ปิดสนิท สำหรับเหตุผลที่ระบุไว้:

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

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

DoubleSided = false

การสร้างโมเดลแบบแข็งใน Studio

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

เครื่องมือการสร้างโมเดลแบบแข็งที่เน้นในแถบเครื่องมือของ Studio
เครื่องมือทางลัดคำอธิบาย
การรวมShiftCtrlG (Windows)
ShiftG (Mac)
รวมชิ้นส่วนสองชิ้นหรือมากกว่ากันเพื่อสร้างการรวมที่เป็นของแข็งเดียว
การตัดกันShiftCtrlI (Windows)
ShiftI (Mac)
ตัดกันชิ้นส่วนที่ทับซ้อนกันให้เป็นการตัดกันที่เป็นของแข็งเดียว
แยกShiftCtrlU (Windows)
ShiftU (Mac)
แยกการรวม หรือการตัดกันกลับไปเป็นชิ้นส่วนเดิม
การลบShiftCtrlN (Windows)
ShiftN (Mac)
ลบชิ้นส่วน ซึ่งมีประโยชน์สำหรับการสร้างรูและรอยบุ๋ม

การรวม

เครื่องมือ การรวม จะรวมชิ้นส่วนสองชิ้นหรือมากกว่ากันเพื่อสร้าง UnionOperation ที่เป็นของแข็งเดียว

บล็อกและกระบอก
ชิ้นส่วนเดี่ยว
บล็อกและกระบอกที่รวมเป็นวัตถุเดียว
ผลลัพธ์การรวม

ในการรวมชิ้นส่วนเข้าด้วยกัน:

  1. เลือกชิ้นส่วนทั้งหมดที่คุณต้องการรวมเข้าด้วยกัน
  2. คลิกปุ่ม การรวม ชิ้นส่วนทั้งหมดจะรวมกันเป็น UnionOperation ที่เป็นของแข็งเดียวโดยมีชื่อว่า การรวม

การตัดกัน

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

บล็อกและกระบอก
ชิ้นส่วนเดี่ยว
บล็อกและกระบอกที่รวมเป็นวัตถุเดียว
ผลลัพธ์การตัดกัน

ในการตัดกันชิ้นส่วนที่ทับซ้อนกัน:

  1. เลือกชิ้นส่วนทั้งหมดที่คุณต้องการตัดกัน
  2. คลิกปุ่ม การตัดกัน ชิ้นส่วนทั้งหมดจะรวมกันเป็น IntersectOperation ที่เป็นของแข็งเดียวโดยมีชื่อว่า การตัดกัน

การลบ

เครื่องมือ การลบ จะลบชิ้นส่วนหนึ่งชิ้นเพื่อให้เมื่อมันถูก รวมกับชิ้นส่วนอื่น รูปร่างของชิ้นส่วนที่ถูกลบจะ ถูกลบ ออกจากชิ้นส่วนอื่น

บล็อกและกระบอก
บล็อกและกระบอกที่ถูกลบ
บล็อกและกระบอกที่รวมเป็นวัตถุเดียว
ผลลัพธ์การลบ

ในการลบชิ้นส่วนจากชิ้นส่วนที่ทับซ้อนกันอื่น:

  1. เลือกชิ้นส่วนที่คุณต้องการลบจากชิ้นส่วนอื่น
  2. คลิก การลบ ชิ้นส่วนจะกลายเป็นโปร่งใสด้วยสีแดงเพื่อบ่งบอกสถานะของมัน
  3. เลือกทั้งชิ้นส่วนที่ถูกลบและชิ้นส่วนที่คุณต้องการลบออกจากมัน
  4. คลิก การรวม ชิ้นส่วนที่ถูกลบจะถูกตัดออกจากชิ้นส่วนที่ทับซ้อนกันที่รวมอยู่

แยก

เครื่องมือ แยก จะทำการแยก UnionOperation กลับไปเป็นชิ้นส่วนเดิม ซึ่งทำหน้าที่เป็นเครื่องมือ "เลิกทำ" สำหรับการรวมและการตัดกัน

ในการแยกการรวม หรือการตัดกันกลับไปเป็นชิ้นส่วนเดิม:

  1. เลือกการรวม หรือการตัดกัน
  2. คลิก แยก ชิ้นส่วนจะกลับไปเป็นรูปแบบเดิม

การสร้างโมเดลแบบแข็งในเกม

คุณยังสามารถทำการดำเนินการสร้างโมเดลแบบแข็งในขณะที่เกมกำลังทำงานหรือจาก ปลั๊กอิน ผ่านวิธี 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:

  1. สร้าง 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 = 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. สร้าง 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 = 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. สร้าง 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 เท่านั้นที่อยู่ภายในชิ้นส่วนที่สองจะส่งผลให้เกิดชิ้นส่วนแยกออกมา ชิ้นส่วนอื่นๆ จะมีเซลล์รวมกันเป็นชิ้นส่วนเดียว

แม่แบบในรูปร่างของโลโก้ 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

สคริปต์ต่อไปนี้เป็นการแทนที่ 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

ผลลัพธ์การสร้างโมเดลแบบแข็ง

รายละเอียดพฤติกรรม

  • ชิ้นส่วนเดิมจะยังคงอยู่หลังจากการดำเนินการที่ประสบความสำเร็จ และชิ้นส่วนที่ส่งกลับไม่มีการตั้งค่า Parent ในกรณีส่วนใหญ่ แนะนำให้จัดพาเรนต์ชิ้นส่วนที่ส่งกลับไปยังสถานที่เดียวกันกับชิ้นส่วนหลัก จากนั้น Destroy() ชิ้นส่วนเดิม

  • หากชิ้นส่วนหลักกำลังเคลื่อนที่ในระหว่างการคำนวณการดำเนินการ คุณสามารถตั้งค่าชิ้นส่วนที่ได้ผลลัพธ์ให้เป็น 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 ไม่แนะนำเนื่องจากอาจทำให้เกิด "เงา" บนการรวมและการตัดกันที่มีขอบคม

©2026 Roblox Corporation. Roblox, โลโก้ Roblox และ Powering Imagination เป็นส่วนหนึ่งของเครื่องหมายการค้าที่จดทะเบียน และไม่ได้จดทะเบียนของเราในสหรัฐฯ และประเทศอื่นๆ