การสร้างรูปทรงแบบกลวง

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

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

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

วัตถุ Mesh ที่สร้างขึ้นด้วย SweepPartAsync()
วัตถุ Mesh ที่ถูกทำลายด้วย FragmentAsync()

รูปทรงที่แน่นทะลุ

มีสามองค์ประกอบพื้นฐานของ Mesh:

  • จุดยอด - จุดเดียวใน Mesh
  • เส้น - เส้นที่เชื่อมต่อสองจุดยอด
  • พื้นผิว - พื้นที่ผิวระหว่างจุดยอดสามจุดขึ้นไป
จุดยอดหนึ่งจุดใน Mesh ลูกบาศก์.
จุดยอด
เส้นหนึ่งเส้นใน Mesh ลูกบาศก์.
เส้น
พื้นผิวหนึ่งพื้นใน Mesh ลูกบาศก์.
พื้นผิว

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

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

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

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

ตัวอย่างของ Mesh ที่ดูแตกต่างเมื่อ DoubleSided ถูกเปิดใช้งาน
นี่คือ Mesh เปลือกนอกและวิธีการซ่อมแซมอัตโนมัติจะไม่สามารถใช้ได้กับมัน.

การสร้างรูปทรงแบบกลวงใน Studio

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

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

การรวมกันของชิ้นส่วน

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

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

เพื่อรวมชิ้นส่วนเข้าด้วยกันเป็นการรวมกัน:

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

ตัดกันของชิ้นส่วน

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

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

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

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

การลบชิ้นส่วน

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

บลอคและทรงกระบอก.
บลอคและทรงกระบอกที่ถูกทำให้เป็นลบ
บลอคและทรงกระบอกที่รวมกันเป็นวัตถุหนึ่ง.
ผลลัพธ์ของการลบ

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

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

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

แยกการรวมกันหรือตัดกัน

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

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

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

การสร้างรูปทรงแบบกลวงในเกม

คุณยังสามารถดำเนินการสร้างรูปทรงแบบกลวงขณะที่เกมกำลังทำงานโดยใช้ฟังก์ชัน GeometryService

UnionAsync(), IntersectAsync(), และ SubtractAsync()

คล้ายกับเครื่องมือการดำเนินการบูลีนพื้นฐานใน 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,
}
-- ทำการรวมกันใน pcall() เนื่องจากเป็นการดำเนินการแบบอะซิงโครนัส
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

ตราบใดที่ข้อมูลที่ป้อนทั้งหมดประกอบด้วยชิ้นส่วนพื้นฐานที่ไม่ใช่ MeshParts, การดำเนินการรวมกัน, การตัดกัน, และการลบจะผลิตการดำเนินการ PartOperation ที่มีข้อมูลที่จัดเก็บอยู่สองชิ้น: ต้นไม้ของการดำเนินการ CSG ที่รู้จักกันในชื่อ CSGTree และ Mesh สำหรับการเรนเดอร์

เมื่อเปรียบเทียบกับ BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync(), ฟังก์ชันบูลีนของ GeometryService จะแตกต่างกันดังนี้:

  • ผลลัพธ์คืออาร์เรย์ของอินสแตนซ์แทนที่จะเป็นอินสแตนซ์เดียว
  • ชิ้นส่วนที่ป้อนไม่จำเป็นต้องถูกจัดให้อยู่ในฉาก ซึ่งอนุญาตให้ทำการดำเนินการในพื้นหลัง
  • เมื่อการตั้งค่า SplitApart ถูกตั้งค่าเป็น true (ค่าเริ่มต้น) รอบของแต่ละชิ้นส่วนที่แตกต่างกันจะถูกคืนค่าใน PartOperation/MeshPart ของตนเอง
  • ชิ้นส่วนที่ส่งกลับทั้งหมดอยู่ในอวกาศพิกัดของชิ้นส่วนหลัก ดังนั้นตำแหน่ง PVInstance.Origin ของพวกมันจะเหมือนกับชิ้นส่วนหลัก สิ่งนี้จะทำให้จุดยอดของ Mesh อยู่ในตำแหน่งเดียวกันสัมพันธ์กับวัตถุตามเดิม แต่ก็หมายความว่าตำแหน่ง (0, 0, 0) ของชิ้นส่วนที่ส่งคืนไม่จำเป็นต้องอยู่ที่ศูนย์กลางของตัวมัน

SweepPartAsync()

ฟังก์ชัน GeometryService: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
รูปร่างเกลียวที่สร้างโดยการดึงทรงกลม.

ตัวอย่าง

รูปร่างโค้งโปร่งแสงถูกตัดออกจากบลอค.

ตัวอย่างนี้ใช้ GeometryService:SweepPartAsync() เพื่อสร้างคุณลักษณะการตัดเกราะซึ่งการเคลื่อนไหวของดาบอิงตามตำแหน่งเมาส์ของผู้เล่น การเคลื่อนไหวของเมาส์ของผู้ใช้จะถูกบันทึกเป็นรายการของ CFrames, SweepPartAsync() จะสร้าง Mesh ที่แบ่งจากข้อมูลนี้ จากนั้น Mesh ที่แบ่งจะถูกตัดออกจากชิ้นส่วนที่ถูกตี

เพื่อให้ตัวอย่างนี้ทำงานใน 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()

ฟังก์ชัน GeometryService:FragmentAsync() และ GeometryService:GenerateFragmentSites() จะทำให้คุณสามารถทำให้ชิ้นส่วนแตกออกเป็นชิ้นเล็ก ๆ ด้วยรูปร่างที่ดูเป็นธรรมชาติ GeometryService:FragmentAsync() ใช้การแยกแบบ Voronoi ในการแบ่งชิ้นส่วนเดียวเป็นหลาย MeshPart โดยพิจารณาจากรูปแบบของจุดที่ส่งเข้ามา ขณะที่ GeometryService: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() สร้างจะเป็นอาร์เรย์ภายในของทั้งหมดสถานที่ที่อยู่นอกบริเวณ radius ที่ขอ หากคุณต้องการทำบางสิ่งที่เฉพาะเจาะจงกับ 'ส่วนที่ไม่แตก' ที่เหลืออยู่ของชิ้นส่วน คุณสามารถค้น ส่วนนั้นได้โดยการตรวจสอบ fragments[i].Index == 1 ขณะวนซ้ำผ่านผลลัพธ์ของ FragmentAsync().


local GeometryService = game:GetService("GeometryService")
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")
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

สคริปต์ต่อไปนี้เป็นกรณีการใช้งานที่เฉพาะเจาะจงมากขึ้น แต่จะแสดงให้เห็นถึงพลังของข้อมูลดัชนีที่ส่งกลับจาก GeometryService:FragmentAsync().

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

การดำเนินการนี้เกี่ยวข้องกับหลายการดำเนินการ Async จึงอาจไม่เหมาะสมในการใช้งานในเกมเป็นการตอบสนองทันทีต่อการป้อนข้อมูลของผู้ใช้เช่นเครื่องมือค้อน

แถวของบล็อก
แถวของบล็อก
แถวของบล็อกที่แตก
แต่ละชิ้นอาจมาจากหลายชิ้นส่วนเดิม

local GeometryService = game:GetService("GeometryService")
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

สคริปต์ต่อไปนี้เป็นการแทนที่ Lua ที่เกือบจะเหมือนกันกับ GeometryService:GenerateFragmentSites(). หากคุณต้องการให้พฤติกรรมที่คล้ายคลึงกับ GeometryService: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

คงการจำกัด

ถ้าชิ้นส่วนที่ป้อนมีการจำกัดหรือการติดตั้งที่คุณต้องการที่จะรักษา คุณสามารถถ่ายโอนมันไปยังชิ้นส่วนที่เกิดขึ้น การกำหนดว่าแต่ละข้อเสนอจะต้องติดอยู่กับชิ้นส่วนที่ได้มานั้นอาจแสดงถึงความยุ่งยาก ดังนั้นจึงแนะนำให้ใช้ GeometryService: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 ของชิ้นส่วนหลัก เนื่องจากชิ้นส่วนที่ส่งกลับอยู่ในอวกาศพิกัดเดียวกับชิ้นส่วนหลัก

  • มีฟังก์ชันเพื่อสลับข้อมูล Mesh ของอินสแตนซ์ ทำให้ง่ายต่อการใช้รูปทรงของการดำเนินการในขณะที่ยังคงรักษาคุณสมบัติ, แอตทริบิวต์, แท็ก และเด็กของชิ้นส่วนหลัก เช่น Attachments, Constraints, ParticleEmitters, วัตถุแสง และ_DECAL_ สิ่งนี้ยังหลีกเลี่ยงการสร้างอาการ "กระพริบ" ของการเปลี่ยนชิ้นส่วน PartOperation เดิมด้วยอีกด้วย

    • หากใช้วิธีนี้กับ PartOperation เป็นชิ้นส่วนหลักและไม่มีชิ้นส่วนอื่นเป็น MeshParts คุณสามารถสลับในรูปทรงของ PartOperation อื่นผ่าน SubstituteGeometry()
    • หากชิ้นส่วนหลักเป็น MeshPart คุณสามารถใช้ MeshPart:ApplyMesh().
  • สามารถทำการเรียกฟังก์ชันเหล่านี้บนไคลเอนต์ได้ แต่มีข้อจำกัดบางอย่าง ก่อนอื่น ต้องทำกับวัตถุที่ สร้างขึ้น บนไคลเอนต์ ประการที่สอง ไม่มีการทำซ้ำจากไคลเอนต์ไปยังเซิร์ฟเวอร์

  • คุณสมบัติต่อไปนี้จากชิ้นส่วนหลักจะถูกนำไปใช้กับ PartOperations หรือ MeshParts ที่เป็นผลลัพธ์:

ข้อพิจารณาผลลัพธ์การสร้างรูปทรงแบบกลวง

สีและ UVs

สีของชิ้นส่วนที่เป็นผลลัพธ์หลังการสร้างรูปทรงแบบกลวงจะมาจากสองแหล่ง: สีหน้าและสีของชิ้นส่วน Color.

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

UVs จะได้รับการจัดการแตกต่างกันไปขึ้นอยู่กับประเภทของผลลัพธ์:

  • PartOperations จะมี UVs แบบ boxmapped เสมอ ซึ่งหมายความว่าทุกพื้นผิวจะมีวัสดุ/พื้นผิว/ข้อความที่มาจากทิศทางหนึ่ง (หนึ่งใน -x, +x, -y, +y, -z, +z) ใช้กับมัน สิ่งนี้อาจทำให้พื้นผิวยืดออก
  • MeshParts ไม่มีการ boxmapped UVs ของ Mesh ของชิ้นส่วนหลัก อาจใช้ UVs ของพื้นผิวที่มาจากชิ้นส่วนอื่นได้เนื่องจาก Roblox ตอนนี้ไม่มีการสนับสนุนหลายวัสดุ UVs ของพื้นผิวที่มาจากชิ้นส่วนอื่นจะถูกมอบให้ UVs ของ (0, 0). เพื่อให้ได้ผลลัพธ์ที่ดีที่สุด ควรตรวจสอบให้แน่ใจว่าจุดพิกเซล (0, 0) ของพื้นผิวของคุณมีสีที่สมเหตุสมผล

มุมการขัด

คุณสมบัติ SmoothingAngle ของชิ้นส่วนที่สร้างขึ้นจะทำให้มุมระหว่างพื้นผิวที่อยู่ติดกันที่มีสีเดียวกันนุ่มนวล มุมที่สูงกว่าจะสร้างให้ปรากฏตัวที่นุ่มนวลยิ่งขึ้น ขณะที่มุมที่ต่ำกว่าจะทำให้ปรากฏตัวที่หยาบขึ้นมีขอบที่คมชัดมากขึ้น

ในขณะที่ค่าใดๆ ระหว่าง 30 ถึง 70 องศามักจะส่งผลให้เกิดผลลัพธ์ที่ดี ค่าเฉลี่ยระหว่าง 90 ถึง 180 ไม่แนะนำเนื่องจากอาจทำให้เกิดผลกระทบ "เงา" บนการรวมกันและการตัดกันที่มีขอบที่คมชัด

การทำให้ชิ้นส่วนเรียบง่าย

หากการดำเนินการสร้างรูปทรงแบบกลวงส่งผลให้ชิ้นส่วนใดๆ มีจำนวนเกิน 20,000 สามเหลี่ยมพวกมันจะถูกทำให้เรียบง่ายลงเป็น 20,000 หากไม่สามารถทำได้ โดยทั่วไปจะเกิดข้อผิดพลาดในกรณีที่มีส่วนประกอบที่ไม่ทับซ้อนเป็นพันๆ

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