النمذجة الصلبة

*This content is translated using AI (Beta) and may contain errors. To view this page in English, click here.

النمذجة الصلبة هي عملية دمج BaseParts معًا بطرق فريدة لتشكيل أشكال أكثر تعقيدًا. يشمل ذلك العمليات البوليانية الاتحاد، التقاطع، والطرح المعروفة عمومًا باسم الهندسة الصلبة الإنشائية (CSG). يمكنك إجراء النمذجة الصلبة في الاستوديو، والإضافات، وفي اللعبة على كل من الخادم والعميل.

بالإضافة إلى CSG البولياني، تدعم النمذجة الصلبة أيضًا الشبكات، طالما أنها محكمة الإغلاق، وعمليات مثل السحب والتجزئة التي تتيح لك وللاعبينك تقطيع، وقطع، وتحطيم الهندسة لتفاعلات لعب فريدة.

MeshPart تم إنشاؤه باستخدام SweepPartAsync()
MeshPart تم كسره باستخدام FragmentAsync()

الهندسة المحكمة

يمكن أن تعمل عمليات النمذجة الصلبة فقط مع الهندسة المحكمة. من الناحية الفنية، تعني الشبكة المحكمة أنها مغلقة، متعددة الأوجه، وغير متقاطعة ذاتيًا. هذه المصطلحات لها تعريفات صارمة، ولكن إليك بعض القواعد البسيطة:

  • يجب أن يكون لكل وجه جانب "داخلي" وجانب "خارجي". يتم تحديد ذلك بواسطة ترتيب لف الوجه، وهو ترتيب رؤوسه الثلاثة.
  • يجب أن يتم مشاركة كل حافة بواسطة مثلثين بالضبط. هذا يعني أنه لا يمكن أن تكون هناك أي ثقوب في الشبكة لأن الحواف حول حافة الثقب سيكون لها مثلث واحد فقط.
  • لا يمكن أن تمر الوجوه عبر وجوه أخرى.
  • يجب أن تتفق المثلثات المجاورة على أي جانب هو الجانب "الخارجي".
  • يجب أن يكون لكل رأس بالضبط مروحة واحدة من المثلثات المجاورة.

كل من الأمثلة التالية غير محكمة للسبب المذكور:

نظام النمذجة الصلبة قادر على إصلاح تلقائي لمشاكل صغيرة معينة في الشبكة، ولكن بشكل عام، ستفشل استدعاءات API إذا لم تكن الشبكة محكمة. لا توجد طريقة واحدة تناسب الجميع لإصلاح شبكة غير محكمة موجودة، ولكن هناك العديد من إضافات Blender التي يمكن أن تساعد، مثل 3D Print Toolbox وMesh Repair Tools. كخيار آخر، يحتوي Meshlab أيضًا على أدوات مفيدة جدًا مدمجة لمحاولة جعل الشبكة متعددة الأوجه، وهو المتطلب الرئيسي لجعل الشبكة محكمة.

إحدى الطرق لرؤية أن الشبكة ستكون صعبة للغاية لجعلها محكمة هي النظر إليها في الاستوديو من جميع الزوايا، ثم محاولة تمكين وتعطيل خاصية MeshPart.DoubleSided. إذا كنت تستطيع رؤية أي فرق، فإن الشبكة مجرد قشرة والأدوات المذكورة أعلاه لن تعمل لأنها لا تستطيع تخمين ما هو الفضاء داخل الشبكة مقابل الخارج. ومع ذلك، إذا كنت تريد فقط شبكة رقيقة وليس من المهم الحفاظ على أبعاد الشبكة بالضبط كما هي، يمكنك استخدام معدل التصلب في Blender لجعل القشرة أكثر سمكًا قليلاً إلى شبكة محكمة.

DoubleSided = false

النمذجة الصلبة في الاستوديو

يمكنك إجراء ثلاث عمليات بوليانية أساسية باستخدام أربع أدوات ضمن شريط أدوات النموذج.

أدوات النمذجة الصلبة المميزة في شريط أدوات الاستوديو.
الأداةاختصارالوصف
الاتحاد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.

العمليات الأساسية

على غرار أدوات العمليات المدمجة في الاستوديو، يمكنك استخدام طرق 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() بإنشاء 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() ببناء شبكة تقطيع من هذه البيانات، ثم يتم طرح شبكة التقطيع من الجزء الذي تم ضربه.

لتشغيل هذا المثال في الاستوديو:

  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() تحليل فورونوي لتقسيم جزء واحد إلى عدة كائنات 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() بإنشائها عبارة عن مصفوفة داخلية لجميع المواقع التي تكون خارج نصف القطر المطلوب. إذا كنت تريد القيام بشيء محدد للجزء "غير المحطم" المتبقي من الجزء، يمكنك العثور على تلك الجزء من خلال التحقق من 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

تقوم الشيفرة التالية بتفكيك الأجزاء من الجزء الأول ضمن شكل محدد باستخدام جزء ثانٍ كقالب. ستؤدي فقط مواقع فورونوي داخل الجزء الثاني إلى قطع منفصلة. ستجمع جميع المواقع الأخرى خلاياها في جزء واحد.

قالب على شكل شعار 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(). إذا كنت تريد سلوكًا مشابهًا لـ 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 الأصلي تمامًا بآخر.

  • من الممكن استدعاء هذه الطرق على العميل، ولكن مع بعض القيود. أولاً، يجب أن يتم ذلك مع كائنات تم إنشاؤها على العميل. ثانيًا، لا يوجد تكرار متاح من العميل إلى الخادم.

  • يتم تطبيق الخصائص التالية من الجزء الرئيسي على PartOperations أو MeshParts الناتجة:

الألوان وUVs

تأتي ألوان الأجزاء الناتجة بعد النمذجة الصلبة من مكانين: ألوان الوجه ولون الجزء Color.

  • إذا كانت النتيجة هي PartOperation، فستكون لها Color من الجزء الأول الذي حددته في الاستوديو، ولكن يستخدم الاستوديو ألوان الوجه بشكل افتراضي للحفاظ على كل وجه بنفس لونها كما كانت قبل العملية. يمكنك تمكين خاصية UsePartColor في الاستوديو لتجاوز هذا السلوك وجعل النتيجة بالكامل بلون واحد.
  • إذا كانت النتيجة هي MeshPart، فسيكون لونها Color أبيض، وستظهر ألوان الوجه دائمًا. يمكنك ضبط لون الأجزاء الناتجة عن طريق تغيير Color، ولكن سيتم مزجه (ضربه) مع ألوان الوجه. هذا يلون النتيجة بدلاً من تجاوز ألوان الوجه تمامًا. إذا كنت تريد التحكم الكامل في لون الناتج، فمن الأفضل جعل المدخلات بيضاء أولاً.

تتم معالجة UVs أيضًا بشكل مختلف اعتمادًا على نوع النتيجة:

  • تحتوي PartOperations دائمًا على UVs مخصصة، مما يعني أن كل وجه سيكون له المادة/القوام/الملصق من اتجاه واحد (واحد من -x، +x، -y، +y، -z، +z) مطبقة عليه. يمكن أن يؤدي ذلك إلى تمدد القوام.
  • MeshParts ليست مخصصة. يتم استخدام UVs لشبكة الجزء الرئيسي. نظرًا لأن Roblox لا تدعم حاليًا المواد المتعددة، فإن UVs للوجوه التي تنشأ من الأجزاء الأخرى تُعطى UVs من (0, 0). للحصول على أفضل النتائج، تأكد من أن البكسل (0, 0) من قوامك له لون معقول.

زاوية التنعيم

تقوم خاصية SmoothingAngle لجزء تم نمذجته صلبًا بتنعيم الزوايا بين الأسطح المجاورة من نفس اللون. تؤدي القيمة الأعلى إلى مظهر أكثر نعومة بينما تؤدي القيمة الأقل إلى مظهر أكثر خشونة مع المزيد من الحواف الحادة.

بينما عادةً ما تنتج قيمة بين 30 و70 درجة نتيجة جيدة، فإن القيم بين 90 و180 غير موصى بها لأنها قد تسبب تأثير "الظل" على الاتحادات والتقاطعات ذات الحواف الحادة.

©2026 شركة Roblox Corporation. تُعد منصّة Roblox، وشعار Roblox وشعار "توسيع حدود المخيلة"، من ضمن علاماتنا التجارية المسجّلة وغير المسجّلة في الولايات المتحدة وبلدان أخرى.