Giống phần 9, ở phần này, chúng ta cũng sẽ tập trung vào việc thực hành viết một script hoàn chỉnh. Mục tiêu của chúng ta là tạo ra một script dùng để chọn các vật thể có số mặt (poly) nhiều nhất trong scene hiện tại.
Nó hữu dụng để kiểm tra xem scene bị “nặng” ở phần nào, và giúp quy trình tối ưu file thuận lợi hơn. Mở MAXScript Edittor lên và cũng bắt đầu.
MỤC LỤC
Bắt đầu viết script
Ở trong 3ds Max, chúng ta đã có sẵn tính năng Select From Scene (phím tắt H) với nhiều bộ lọc hoàn chỉnh, trong đó có cả chọn các vật thể nhiều mặt nhất. Tuy nhiên, cách làm việc của nó hơi lòng vòng. Phải click kha khá lần để đạt được mục đích của chúng ta. Do đó, tôi cùng bạn sẽ viết một script để thực hiện điều này.
Tạo cửa sổ chính
Trước hết, chúng ta cần thiết kế một cửa sổ cơ bản trong MAXScript.
try destroyDialog SelectTopNObjectsRollout catch() -- thử đóng cửa sổ SelectTopNObjectsRollout rollout SelectTopNObjectsRollout "Select Top N Objects by Polycount" ( -- nội dung rollout ) createDialog SelectTopNObjectsRollout 300 100
Trong phần trước, tôi đã giải thích về cặp lệnh try – catch. Nếu bạn vẫn còn bỡ ngỡ với lệnh này, hãy xem lại trước khi tiếp tục.
Sau khi thực thi chuỗi lệnh trên trong MAXScript, hộp thoại có kích thước 300×100 pixel sẽ xuất hiện. Chúng ta cần tiếp tục bổ sung thêm các thành phần giao diện cho nó.
Với script này, tôi sẽ cần một chỗ cho người dùng nhập liệu (số vật thể), và một nút bấm để bắt đầu hành động chọn vật thể.
spinner numObjects "Number of Objects: " range:[1, 1000, 10] type:#integer align:#center button selectButton "Select" align:#center
Thêm đoạn trên thay thế cho dòng – – nội dung rollout. Chạy thử code, chúng ta được một giao diện cơ bản như hình dưới.
Giải thích về giao diện
Trong đây, tôi sử dụng hai thành phần giao diện là spinner và button. Spinner là một hộp số để người dùng nhập số lượng đối tượng cần chọn. Button là một nút bấm với tiêu đề là “Select”. Tất cả chúng đều được căn giữa giao diện bằng thuộc tính align:#center.
Ở đây, tôi sẽ chỉ giải thích về spinner, vì button là một thành phần giao diện khá quen thuộc, và tôi sẽ không giới thiệu thêm. Spinner là một hộp số cho phép người dùng chọn giá trị bằng cách nhấn vào mũi tên lên/xuống hoặc nhập trực tiếp số vào (xem hình trên).
numObjects là tên biến mà chúng ta đặt cho spinner này. Tên biến này được sử dụng trong mã nguồn để truy cập giá trị mà người dùng đã chọn trong spinner. Nó không hiển thị trong giao diện người dùng 3ds Max.
“Number of Objects: “ là nhãn (label) của spinner. Nhãn này hiển thị bên cạnh spinner trong giao diện người dùng, giúp người dùng hiểu rõ chức năng của spinner.
range:[1, 1000, 10] tham số xác định khoảng giá trị mà spinner có thể nhận. Trong đó 1 là giá trị tối thiểu mà spinner có thể nhận (chọn ít nhất 1 vật thể), 1000 là giá trị tối đa mà spinner có thể nhận (chọn tối đa 1000 vật thể), 10 là giá trị mặc định khi spinner được mở script.
type:#integer để xác định kiểu dữ liệu của spinner là số nguyên. Điều này nghĩa là người dùng không thể nhập số thập phân vào spinner.
Viết chức năng cho script
Chúng ta đã làm xong phần “vỏ” cho script, và bây giờ cần bắt tay vào phần “lõi”. Mục tiêu của chúng ta là khi bấm nút Select, script sẽ chọn top n đối tượng có nhiều polygon nhất trong scene, với n là số được nhập trong spinner numObjects.
fn getPolyCount obj = try obj.mesh.numfaces catch 0 fn comparePolyCount a b = case of ( (a[2] > b[2]): -1 (a[2] < b[2]): 1 (a[2] = b[2]): 0 ) on selectButton pressed do ( objPolyCounts = for obj in objects collect #(obj, getPolyCount obj) qsort objPolyCounts comparePolyCount select (for i = 1 to numObjects.value collect (objPolyCounts[i])[1]) )
Hãy nhớ rằng chúng ta cần đưa phần code này vào trong dấu đóng mở ngoặc của rollout. Bởi selectButton là một biến local chỉ xuất hiện trong rollout SelectTopNObjectsRollout, nếu đưa ra ngoài, chúng ta phải gọi tên nút bấm rất dài (SelectTopNObjectsRollout.selectButton).
Có thể bạn đang sửng sốt khi tôi xả thẳng một đoạn code dài loằng ngoằng với toàn từ khóa mới. Xin đừng lo lắng, tôi sẽ giải thích chi tiết từng dòng một. Hãy đọc đi đọc lại. Nó có thể khiến bạn nhức đầu chút lúc đầu. Nhưng một khi đã hiểu cách vận hành, bạn sẽ thấy nó mọi thứ cực kỳ đơn giản.
Giải thích các hàm (function)
Trong script này, chúng ta có 2 hàm là getPolyCount và comparePolyCount,
fn getPolyCount obj = try obj.mesh.numfaces catch 0
- fn getPolyCount obj: Định nghĩa một hàm có tên là getPolyCount nhận một tham số obj.
- try obj.mesh.numfaces: Thử lấy số lượng mặt (faces) của mesh của đối tượng obj.
- catch 0: Nếu có lỗi xảy ra (ví dụ, đối tượng không có thuộc tính mesh), trả về giá trị 0.
Tức là, hàm này sẽ trả về số lượng mặt của đối tượng nếu có, nếu không thì trả về 0.
fn comparePolyCount a b = case of ( (a[2] > b[2]): -1 (a[2] < b[2]): 1 (a[2] = b[2]): 0 )
- fn comparePolyCount a b: Định nghĩa một hàm có tên là comparePolyCount nhận hai tham số a và b.
- case of: Cấu trúc điều kiện kiểm tra các giá trị của a và b. Trong trường hợp này, a và b là các array.
- (a[2] > b[2]): -1: Nếu giá trị thứ hai của a lớn hơn giá trị thứ hai của b, trả về -1 (a đứng trước b trong sắp xếp giảm dần).
- (a[2] < b[2]): 1: Nếu giá trị thứ hai của a nhỏ hơn giá trị thứ hai của b, trả về 1 (a đứng sau b).
- (a[2] = b[2]): 0: Nếu giá trị thứ hai của a bằng giá trị thứ hai của b, trả về 0 (a và b có vị trí tương đương).
Ví dụ, nếu tôi có 2 array là #(KhoiBox1, 500) và #(KhoiBox2, 900), hàm trên sẽ so sánh giá trị 500 và 900 (giá trị thứ 2) của 2 array. Vì 500 < 900, nên hàm trả về giá trị là 1, và array chứa KhoiBox2 được đưa lên trước. Tôi sẽ giải thích hàm này kỹ hơn phía dưới.
Giải thích khối lệnh chính
Trong script này, thực tế code chỉ cần chạy và tính toán khi người dùng bấm nút Select (on selectButton pressed do). Còn khi người dùng thay đổi số trong spinner, không có gì xảy ra.
on selectButton pressed do ( objPolyCounts = for obj in objects collect #(obj, getPolyCount obj) qsort objPolyCounts comparePolyCount select (for i = 1 to numObjects.value collect (objPolyCounts[i])[1]) )
Khối lệnh này gồm 3 dòng chính. Dòng đầu dùng để tạo một array tổng chứa các array nhỏ hơn. Array nhỏ có cấu trúc dạng #(vật thể, số mặt). Dòng thứ 2 để sắp xếp array tổng thể thứ tự số mặt giảm dần.
Tạo array chứa các array (array lồng nhau)
Dòng đầu tiên dùng để tạo một mảng objPolyCounts chứa các cặp giá trị. Mỗi cặp bao gồm một đối tượng và số lượng polygon của nó, được tính toán bởi hàm getPolyCount.
Ví dụ, với scene ví dụ hiện tại của tôi, tôi nhận được kết quả sau:
"#(#($Box:Box001 @ [17.889088,13.118664,0.000000], 12), #($Editable_Mesh:Box002 @ [-109.688232,2311.719238,0.000000], 48), #($Box:Box003 @ [3764.990723,-5867.137207,0.000000], 12), #($Editable_Mesh:Box004 @ [3637.413330,-3568.536621,0.000000], 48), #($Box:Box005 @ [7512.092285,-11747.392578,0.000000], 12), #($Editable_Mesh:Box006 @ [7384.514648,-9448.792969,0.000000], 48), #($Box:Box007 @ [11259.193359,-17627.648438,0.000000], 12), #($Editable_Mesh:Box008 @ [11131.616211,-15329.048828,0.000000], 48), #($Box:Box009 @ [15006.294922,-23507.904297,0.000000], 12), #($Editable_Mesh:Box010 @ [14878.717773,-21209.304688,0.000000], 48), #($Box:Box011 @ [18753.396484,-29388.160156,0.000000], 12), #($Editable_Mesh:Box012 @ [18625.820313,-27089.560547,0.000000], 48), #($CoronaLight:CoronaLight001 @ [30108.148438,-10967.372070,0.000000], 0), #($CoronaLight:CoronaLight002 @ [33984.078125,-19049.046875,0.000000], 0), #($CoronaLight:CoronaLight003 @ [27093.398438,-23975.380859,0.000000], 0))"
Trông thật khủng khiếp phải không? Nhưng hãy bình tĩnh nhìn lại, nó không phức tạp như bạn nghĩ. Đây thực ra là một array chứa các array nhỏ với cấu trúc như sau: #(a @ b, c). Trong đó, a là biến xác định vật thể, nằm ở tọa độ b, với c là tổng số poly được tính bằng hàm getPolyCount.
Ví dụ ở array đầu tiên: $Box:Box001 có nghĩa là khối Box tên Box001, nằm ở [x, y, z] = [17.889088 ,13.118664, 0.000000], và có tổng số mặt là 12.
Hãy lưu ý, có một vài con số 0 trong đoạn trên. Điều này có nghĩa là lệnh catch trong hàm getPolyCount đã hoạt động.
Sắp xếp array theo số poly
Dòng lệnh tiếp theo dùng hàm qsort có sẵn trong MAXScript. Nó là một hàm dùng để sắp xếp một mảng (array) theo một tiêu chí cụ thể mà người dùng định nghĩa. Cú pháp như sau:
qsort array compareFunction
- array: Mảng cần được sắp xếp.
- compareFunction: Hàm so sánh được sử dụng để quyết định thứ tự các phần tử trong mảng.
Hàm so sánh compareFunction sẽ do người dùng cung cấp. Nó nhận hai tham số tương ứng với hai phần tử trong mảng, và cần trả về giá trị:
- -1 nếu phần tử đầu tiên nên đứng trước phần tử thứ hai.
- 1 nếu phần tử đầu tiên nên đứng sau phần tử thứ hai.
- 0 nếu hai phần tử có thứ tự ngang nhau.
Quay lại với hàm qsort comparePolyCount của chúng ta. Chúng ta so sánh 2 array chứa vật thể, bằng thuộc tính số mặt. Vật thể nào có số mặt nhiều hơn sẽ được xếp trước.
Tức là khi so sánh #(vật_1, số_mặt_1) với #(vật_2, số_mặt_2), nó sẽ so sánh số_mặt_1 và số_mặt_2 để quyết định xem cái nào nên được đặt trước.
Với ví dụ trên, sau khi lệnh qsort được chạy, mảng trả về như sau:
"#(#($Editable_Mesh:Box008 @ [11131.616211,-15329.048828,0.000000], 48), #($Editable_Mesh:Box002 @ [-109.688232,2311.719238,0.000000], 48), #($Editable_Mesh:Box010 @ [14878.717773,-21209.304688,0.000000], 48), #($Editable_Mesh:Box004 @ [3637.413330,-3568.536621,0.000000], 48), #($Editable_Mesh:Box012 @ [18625.820313,-27089.560547,0.000000], 48), #($Editable_Mesh:Box006 @ [7384.514648,-9448.792969,0.000000], 48), #($Box:Box001 @ [17.889088,13.118664,0.000000], 12), #($Box:Box009 @ [15006.294922,-23507.904297,0.000000], 12), #($Box:Box003 @ [3764.990723,-5867.137207,0.000000], 12), #($Box:Box011 @ [18753.396484,-29388.160156,0.000000], 12), #($Box:Box007 @ [11259.193359,-17627.648438,0.000000], 12), #($Box:Box005 @ [7512.092285,-11747.392578,0.000000], 12), #($CoronaLight:CoronaLight002 @ [33984.078125,-19049.046875,0.000000], 0), #($CoronaLight:CoronaLight003 @ [27093.398438,-23975.380859,0.000000], 0), #($CoronaLight:CoronaLight001 @ [30108.148438,-10967.372070,0.000000], 0))"
Các phần tử đã được sắp xếp với số mặt giảm dần. Giờ chúng ta sẽ dùng lệnh để chọn nó.
Chọn các vật thể nhiều poly nhất
Chúng ta sẽ dùng lệnh select để chọn vật thể. Tuy nhiên, lệnh này chỉ cho chọn vật thể hoặc array vật thể. Nó sẽ báo lỗi nếu chúng ta cho nó chọn trực tiếp các phần tử trong mảng objPolyCounts. Vì thế, đầu tiên chúng ta cần phải xây dựng một mảng array chỉ chứa các vật thể cần chọn. Vòng lặp for i = 1 to x collect sẽ được sử dụng (xem lại bài về vòng lặp).
for i = 1 to numObjects.value collect (objPolyCounts[i])[1]
Hãy để ý từ khóa numObjects.value. Nó chính là con số mà người dùng nhập vào trong spinner numObjects. Đây cũng là thời điểm duy nhất script cần sử dụng đến con số này. Vòng lặp sẽ chạy từ 1 đến n (số người dùng nhập vào), và thu thập (collect) một giá trị tương ứng là (objPolyCounts[i])[1], tức là phần tử đầu tiên [1], của objPolyCounts[i].
Ví dụ, với n = 10, tôi có kết quả:
"#($Editable_Mesh:Box008 @ [11131.616211,-15329.048828,0.000000], $Editable_Mesh:Box002 @ [-109.688232,2311.719238,0.000000], $Editable_Mesh:Box010 @ [14878.717773,-21209.304688,0.000000], $Editable_Mesh:Box004 @ [3637.413330,-3568.536621,0.000000], $Editable_Mesh:Box012 @ [18625.820313,-27089.560547,0.000000], $Editable_Mesh:Box006 @ [7384.514648,-9448.792969,0.000000], $Box:Box001 @ [17.889088,13.118664,0.000000], $Box:Box009 @ [15006.294922,-23507.904297,0.000000], $Box:Box003 @ [3764.990723,-5867.137207,0.000000], $Box:Box011 @ [18753.396484,-29388.160156,0.000000])"
Để ý rằng, array này không chứa số mặt, vì ta chỉ thu thập phần obj của mỗi phần tử #(obj, getPolyCount obj).
Chạy thử script
Sau khi ghép tất cả các code, chúng ta có script hoàn chỉnh như sau:
try destroyDialog SelectTopNObjectsRollout catch() rollout SelectTopNObjectsRollout "Select Top N Objects by Polycount" ( spinner numObjects "Number of Objects: " range:[1, 1000, 10] type:#integer align:#center button selectButton "Select" align:#center fn getPolyCount obj = try obj.mesh.numfaces catch 0 fn comparePolyCount a b = case of ( (a[2] > b[2]): -1 (a[2] < b[2]): 1 default: 0 ) on selectButton pressed do ( objPolyCounts = for obj in objects collect #(obj, getPolyCount obj) qsort objPolyCounts comparePolyCount select (for i = 1 to numObjects.value collect (objPolyCounts[i])[1]) ) ) createDialog SelectTopNObjectsRollout 300 100
Đừng lười biếng! Hãy viết lại từng dòng vào trong MAXScript Edittor của bạn, và suy ngẫm về từng dòng. Nó sẽ giúp ích rất nhiều trong việc hiểu cách vận hành của script này. Đây là một bài học phức tạp, vì thế bạn có thể cần một khoản thời gian nhất định để mày mò với nó. Hãy bình luận nếu bạn cần giải đáp ở bất kỳ chỗ nào.
Sau khi chạy, script này đã cho kết quả (tạm thời) như mong muốn với scene tôi test.
Nhưng, có nên dừng ở đây?
Bài tập thực hành
Khác với phần trước, tôi sẽ không đồng hành cùng bạn fix bug script. Ở phần này, bạn phải tự làm nó, như là một bài tập thực hành. Script trên có một số hạn chế, và bạn cần phải suy nghĩ khắc phục. Tôi cũng sẽ cho bạn một gợi ý về cải tiến script, hãy suy nghĩ và phát triển nó.
Những bài tập này không có một lời giải cụ thể vì nó có rất nhiều cách làm. Tuy nhiên tôi cũng sẽ đưa ra một vài đề nghị và gợi ý dành cho bạn. Như các phần trước, dùng chuột bôi đen phần phía dưới mỗi đề bài để thấy đáp án.
Bài tập 1
Script trên sẽ báo lỗi khi nhập n = 10 và trong scene chỉ có 9 vật thể. Hãy khắc phục vấn đề này.
Hãy xử lý biến numObjects.value để đảm bảo nó luôn luôn nhỏ hơn số vật thể hiện tại trong scene.
Bài tập 2
Script trên sẽ chọn cả đèn, camera,…, và những vật thể không có mặt nào nếu số n đủ lớn. Hãy cải thiện vấn đề này.
Hãy xây vòng lặp có điều kiện where để loại những vật thể không có chứa poly nào.
Bài tập 3
Hãy thêm các nút bấm Top 1, Top 3, Top 5,…, Top 100 như một preset sẵn để người dùng không cần nhập số n nữa.
Hãy biến cụm lệnh chính thành một function nhận tham số n. Xây dựng các nút bấm với n tương ứng.