SceneSetup.ms 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. --
  2. -- Command & Conquer Renegade(tm)
  3. -- Copyright 2025 Electronic Arts Inc.
  4. --
  5. -- This program is free software: you can redistribute it and/or modify
  6. -- it under the terms of the GNU General Public License as published by
  7. -- the Free Software Foundation, either version 3 of the License, or
  8. -- (at your option) any later version.
  9. --
  10. -- This program is distributed in the hope that it will be useful,
  11. -- but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. -- GNU General Public License for more details.
  14. --
  15. -- You should have received a copy of the GNU General Public License
  16. -- along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. --
  18. ------------------------------------------------------------------------
  19. --
  20. -- SceneSetup.ms - This script allows an artist to set up their MAX
  21. -- scene to contain multiple LOD and damage models. It contains various
  22. -- functions that the artist can invoke manually if they wish. Those
  23. -- functions are named in "studlyCaps". Functions intended to be used
  24. -- internally have lowercase names with underscores.
  25. --
  26. ------------------------------------------------------------------------
  27. struct SS_bounding_box ( _min = [0,0,0], _max = [0,0,0] )
  28. -- SS_replace_extension provides the same functionality as the "Assign
  29. -- Extensions" button on the W3D tools panel, except in script.
  30. function SS_replace_extension
  31. name_str
  32. new_extension
  33. = (
  34. -- If there's no extension, return the given name.
  35. if new_extension == undefined then
  36. return name_str
  37. -- Find the "." in the name. If there is none, just tack the
  38. -- extension on then end and return it.
  39. local dot_index = findString name_str "."
  40. if dot_index == undefined then
  41. (
  42. if new_extension.count == 1 then
  43. return name_str + ".0" + new_extension
  44. else
  45. return name_str + "." + new_extension
  46. )
  47. -- Replace the two characters after the dot with the new extension.
  48. local replace_str
  49. if new_extension.count == 1 then
  50. replace_str = "0" + new_extension
  51. else
  52. replace_str = new_extension
  53. return (replace name_str (dot_index+1) 2 replace_str)
  54. )
  55. -- SS_clone_tree is used to clone a whole hierarchy of objects. The cloning
  56. -- procedure to be used is passed in as an argument, making this a very
  57. -- flexible function. It operates recursively, cloning each object and
  58. -- maintaining each object's place in the hierarchy and its W3D AppData.
  59. function SS_clone_tree
  60. tree_root
  61. offset:[-100,0,0]
  62. parent:undefined
  63. clone_proc:reference
  64. extension:undefined
  65. = (
  66. -- Create a new object that is a clone of the given one.
  67. local new_object = clone_proc tree_root
  68. -- Change the extension of this node's name if we were given one.
  69. if extension != undefined then
  70. (
  71. local new_name = SS_replace_extension new_object.name extension
  72. if new_name != undefined then
  73. new_object.name = new_name
  74. )
  75. -- Copy the AppData attached to the tree_root to the new node.
  76. wwCopyAppData new_object tree_root
  77. -- Move the new object by the given offset.
  78. move new_object offset
  79. -- Attach the object to its parent in the new tree (if it's not undefined).
  80. if parent != undefined then
  81. attachObjects parent new_object move:false
  82. -- Dupe all the children of the current root.
  83. for child in tree_root.children do
  84. (
  85. SS_clone_tree child offset:offset parent:new_object \
  86. clone_proc:clone_proc extension:extension
  87. )
  88. -- Return the new root.
  89. return new_object
  90. )
  91. function SS_duplicate_skin_info
  92. source_root
  93. target_root
  94. new_wsm:undefined -- WWSkin WSM to attach target objects to
  95. tree:undefined -- root node of the target tree
  96. = (
  97. if tree == undefined then tree = target_root
  98. if source_root.modifiers["WWSkin Binding"] != undefined then
  99. (
  100. -- Copy the skin info for this object into the target object.
  101. -- If we haven't copied the WSM yet, it will be copied.
  102. local retval = wwCopySkinInfo source_root target_root new_wsm tree
  103. if retval == undefined then
  104. (
  105. print("Error copying skin info from " + source_root.name + \
  106. " to " + target_root.name)
  107. )
  108. else new_wsm = retval
  109. )
  110. -- Duplicate the skin info for all of our children
  111. local i
  112. for i = 1 to source_root.children.count do
  113. (
  114. local source_child = source_root.children[i]
  115. local target_child = target_root.children[i]
  116. new_wsm = SS_duplicate_skin_info source_child target_child \
  117. new_wsm:new_wsm tree:tree
  118. )
  119. return new_wsm
  120. )
  121. -- SS_create_lod_models creates a number of LOD models based on the given
  122. -- hierarchy root. Each LOD is cloned from the previous one (as opposed
  123. -- to them all being clones of the root).
  124. function SS_create_lod_models
  125. number
  126. tree_root
  127. offset:[-100,0,0]
  128. clone_proc:reference
  129. = (
  130. local lod_roots = #()
  131. local previous_lod = tree_root
  132. for i = 1 to number do
  133. (
  134. local ext = i as string
  135. lod_roots[i] = SS_clone_tree previous_lod offset:offset \
  136. parent:previous_lod.parent clone_proc:clone_proc \
  137. extension:ext
  138. local original_wsm = wwFindSkinNode previous_lod
  139. if original_wsm != undefined then
  140. (
  141. -- Find the WSM we cloned.
  142. local cloned_wsm = wwFindSkinNode lod_roots[i]
  143. if cloned_wsm == undefined then
  144. print "Warning: A WWSkin object was found but it wasn't linked to the base object!"
  145. else
  146. (
  147. -- Duplicate the WWSkin WSM (but meshes will not be bound to it).
  148. -- ie. The duplicated WSM will contain the correct bone names.
  149. local wsm = wwDuplicateSkinWSM original_wsm lod_roots[i]
  150. if wsm == undefined then
  151. messageBox("Error: Unable to duplicate the skin object for LOD " + ext)
  152. else
  153. (
  154. -- Copy position, name, etc.
  155. wsm.name = cloned_wsm.name
  156. wsm.transform = cloned_wsm.transform
  157. wsm.parent = cloned_wsm.parent
  158. delete cloned_wsm
  159. -- Create a new WWSkin WSM for this LOD with no bindings at all.
  160. --local wsm = WWSkinSpaceWarp()
  161. --wsm.name = SS_replace_extension original_wsm.name ext
  162. --wsm.transform = original_wsm.transform
  163. --move wsm offset
  164. --wsm.parent = lod_roots[i]
  165. )
  166. )
  167. )
  168. print ("Built model: " + lod_roots[i].name)
  169. previous_lod = lod_roots[i]
  170. )
  171. -- Return the array of LOD root nodes in the order they were created.
  172. return lod_roots
  173. )
  174. -- SS_create_damage_models creates a number of LOD models based on the
  175. -- given hierarchy root. Each damage model is cloned from the root.
  176. function SS_create_damage_models
  177. number
  178. tree_root
  179. offset:[0,-100,0]
  180. clone_proc:reference
  181. = (
  182. local apply_offset = offset
  183. local damage_roots = #()
  184. for i = 1 to number do
  185. (
  186. local ext = i as string
  187. damage_roots[i] = SS_clone_tree tree_root offset:apply_offset \
  188. parent:tree_root.parent clone_proc:clone_proc \
  189. extension:ext
  190. -- Rename the damage root from "Origin.xx" to "Damage.%02d",i
  191. if i < 10 then
  192. damage_roots[i].name = "Damage.0" + ext
  193. else
  194. damage_roots[i].name = "Damage." + ext
  195. -- If there is an object bound to a WWSkin WSM, duplicate the
  196. -- WSM and copy the skin information over to the new object.
  197. local wsm = SS_duplicate_skin_info tree_root damage_roots[i]
  198. -- Skin information has been duplicated, now replace the cloned
  199. -- skin WSM with the one we just created with the correct data.
  200. if wsm != undefined then
  201. (
  202. local cloned_wsm = wwFindSkinNode damage_roots[i]
  203. if cloned_wsm != undefined then
  204. (
  205. wsm.name = cloned_wsm.name
  206. wsm.transform = cloned_wsm.transform
  207. wsm.parent = cloned_wsm.parent
  208. delete cloned_wsm
  209. )
  210. else
  211. print "Warning: A WWSkin object was found but it wasn't linked to the base object!"
  212. )
  213. print ("Built model: " + damage_roots[i].name)
  214. -- Shift the next model by offset again.
  215. apply_offset = apply_offset + offset
  216. )
  217. -- Return the array of damage root nodes in the order they were created.
  218. return damage_roots
  219. )
  220. -- SS_query_integer prompts the user to input an integer value. This
  221. -- interaction takes place in the pink/white window in the bottom left
  222. -- of the MAX interface, making it a fallback UI.
  223. function SS_query_integer
  224. prompt:
  225. = (
  226. local number = getKBValue prompt:prompt
  227. if classOf number != integer then
  228. throw "The number must be an integer!"
  229. return number
  230. )
  231. -- SS_get_tree_bbox figures out the bounding box of a hierarchical model.
  232. -- Returns a SS_bounding_box struct containing the min and max points of the
  233. -- node and its children.
  234. function SS_get_tree_bbox
  235. root
  236. bbox:undefined
  237. = (
  238. if root == undefined then return undefined
  239. if bbox == undefined then
  240. (
  241. bbox = SS_bounding_box _min:root.min _max:root.max
  242. )
  243. else
  244. (
  245. if root.min.x < bbox._min.x then
  246. bbox._min.x = root.min.x
  247. if root.min.y < bbox._min.y then
  248. bbox._min.y = root.min.y
  249. if root.min.z < bbox._min.z then
  250. bbox._min.z = root.min.z
  251. if root.max.x > bbox._max.x then
  252. bbox._max.x = root.max.x
  253. if root.max.y > bbox._max.y then
  254. bbox._max.y = root.max.y
  255. if root.max.z > bbox._max.z then
  256. bbox._max.z = root.max.z
  257. )
  258. for child in root.children do
  259. bbox = SS_get_tree_bbox child bbox:bbox
  260. return bbox
  261. )
  262. -- SS_get_set_bbox figures out the bounding box of an ObjectSet.
  263. function SS_get_set_bbox
  264. object_set
  265. bbox:undefined
  266. = (
  267. if object_set == undefined then return undefined
  268. bbox = SS_bounding_box _min:object_set.min _max:object_set.max
  269. return bbox
  270. )
  271. -- SS_get_origin_size figures out how big to make the origin cube based
  272. -- on the size of the bounding box of the given object (including it's
  273. -- children in the bbox calculation).
  274. function SS_get_origin_size
  275. object_set
  276. = (
  277. bbox = SS_get_set_bbox object_set
  278. if bbox == undefined then
  279. (
  280. print "Bounding box calculation error for new origin, defaulting" \
  281. " to origin of size 5"
  282. return 5.0
  283. )
  284. -- Figure out the smallest bounding box dimension.
  285. local min_size = bbox._max.x - bbox._min.x
  286. if (bbox._max.y - bbox._min.y) < min_size then
  287. min_size = bbox._max.y - bbox._min.y
  288. if (bbox._max.z - bbox._min.z) < min_size then
  289. min_size = bbox._max.z - bbox._min.z
  290. -- Origin size will be 1/6 of the smallest bounding box dimension.
  291. return min_size / 6
  292. )
  293. -- SS_create_origin creates a box to serve as the origin of a model. This is used
  294. -- by sceneSetup if there is no Origin.00 node present in the scene.
  295. function SS_create_origin
  296. name:"Origin.00"
  297. = (
  298. -- Figure out how big our origin should be. Its size will be based
  299. -- on the bounding box of the objects in the scene.
  300. local origin_size = SS_get_origin_size geometry
  301. -- Create a new box, and rename it to the given name.
  302. -- The origin is created very small so that it doesn't interfere with
  303. -- the bounding box calculations later on when we figure out exactly
  304. -- how big the origin should be!
  305. local origin = Box width:origin_size height:origin_size length:origin_size
  306. origin.name = name
  307. origin.pos.z -= origin_size / 2
  308. origin.pivot.z = 0
  309. -- Set the box's AppData to the appropriate values for an origin node.
  310. wwSetOriginAppData origin
  311. return origin
  312. )
  313. -- SS_build_hierarchy attaches all top-level objects to the given root node.
  314. -- It then assigns ALL OBJECTS IN THE SCENE an extension of ".00"
  315. function SS_build_hierarchy
  316. root
  317. = (
  318. -- Attach all top-level objects to the given root.
  319. local top_level = $/*
  320. for obj in top_level do
  321. (
  322. if obj == root then continue
  323. print ("Attaching " + obj.name + " to " + root.name)
  324. attachObjects root obj move:false
  325. )
  326. -- Append a ".00" extension to ALL objects in the scene.
  327. for obj in objects do
  328. (
  329. local new_name = SS_replace_extension obj.name "00"
  330. if new_name != undefined then
  331. obj.name = new_name
  332. )
  333. )
  334. -- USER-CALLABLE: Set up LODs based on the Origin.00 hierarchy (by default).
  335. -- The user can specify the number of LODs to create, how much to offset
  336. -- each model by, the method of cloning (copy, instance, reference), and
  337. -- the root of the hierarchy the LODs should be based on.
  338. --
  339. -- Sample call:
  340. -- createLOD count:2 offset:[-2,0,0] clone_by:copy root:$'Origin.02'
  341. function createLOD
  342. count:-1 -- default of -1 means prompt the user
  343. offset:[-100,0,0] -- offset by -100 on X axis by default
  344. clone_by:reference -- default to cloning by reference
  345. root:undefined -- clone Origin.00 if not specified
  346. = (
  347. if root == undefined then
  348. root = $'Origin.00'
  349. if root == undefined then
  350. (
  351. -- Create the origin node and link up a hierarchy.
  352. root = SS_create_origin()
  353. SS_build_hierarchy root
  354. )
  355. -- Query the user for the number of LODs to create if she didn't supply one.
  356. local num_lods = count
  357. if num_lods == -1 then
  358. num_lods = SS_query_integer prompt:"Number of LODs to create:"
  359. -- Create LODs by cloning the hierarchy 'count' times.
  360. local lod_roots = SS_create_lod_models num_lods root offset:offset \
  361. clone_proc:clone_by
  362. )
  363. -- USER-CALLABLE: Set up Damage models based on the Origin.00 hierarchy
  364. -- (by default). The user can specify the number of damage models to
  365. -- create, how much to offset each model by, the method of cloning
  366. -- (copy, instance, reference), and the root of the hierarchy the models
  367. -- should be based on.
  368. --
  369. -- Sample call:
  370. -- createDamage count:3 offset:[0,-2,0] clone_by:instance root:$'Origin.00'
  371. function createDamage
  372. count:-1 -- default of -1 means prompt the user
  373. offset:[0,-100,0] -- offset each model by -100 on the Y axis
  374. clone_by:reference -- default to cloning by reference
  375. root:undefined -- default to Origin.00 if not supplied
  376. = (
  377. if root == undefined then
  378. root = $'Origin.00'
  379. if root == undefined then
  380. (
  381. -- Create the origin node and link up a hierarchy.
  382. root = SS_create_origin()
  383. SS_build_hierarchy root
  384. )
  385. -- Query the user for the number of Damage models to create if she didn't supply one.
  386. local num_damage = count
  387. if num_damage == -1 then
  388. num_damage = SS_query_integer prompt:"Number of Damage models:"
  389. -- Create the damage models by cloning the hierarchy 'count' times.
  390. local damage_roots = SS_create_damage_models num_damage root offset:offset \
  391. clone_proc:clone_by
  392. )
  393. -- USER-CALLABLE: Displays a friendly dialog where the user can choose a number
  394. -- of settings for how the scene should be set up. This function has no arguments
  395. -- and will be displayed as a button on the MAX UI.
  396. --
  397. -- Sample call:
  398. -- sceneSetup()
  399. function sceneSetup
  400. = (
  401. -- Figure out some reasonable values for the lod and damage offsets.
  402. -- These values will be plugged into the dialog that prompts the user
  403. -- for values.
  404. local bbox
  405. if $'Origin.00' != undefined then
  406. bbox = SS_get_tree_bbox $'Origin.00'
  407. else
  408. bbox = SS_get_set_bbox geometry
  409. local x_offset = (bbox._max.x - bbox._min.x) * -1.5
  410. local y_offset = (bbox._max.y - bbox._min.y) * -1.5
  411. -- Create an array of default values that will be displayed in the dialog.
  412. -- (lod_count, lod_offset, lod_clone_proc, damage_count, damage_offset,
  413. -- damage_clone_proc)
  414. -- for procs: 1==copy 2==instance 3==reference
  415. --local default_args = #(2, -2, 3, 3, -3, 3)
  416. local default_args = #(2, x_offset, 3, 3, y_offset, 3)
  417. -- Show the dialog to get the parameters from the user.
  418. -- The user's choices will override the above default values.
  419. local chosen_args = wwSceneSetup default_args
  420. if chosen_args != undefined then
  421. (
  422. -- Pick the user's choices out of the array returned from wwSceneSetup.
  423. local lod_count = chosen_args[1]
  424. local lod_offset = chosen_args[2]
  425. local lod_clone_proc = chosen_args[3]
  426. local damage_count = chosen_args[4]
  427. local damage_offset = chosen_args[5]
  428. local damage_clone_proc = chosen_args[6]
  429. local lod_proc
  430. local damage_proc
  431. -- Choose the clone procs
  432. case lod_clone_proc of
  433. (
  434. 1: lod_proc = copy
  435. 2: lod_proc = instance
  436. 3: lod_proc = reference
  437. default: throw "Invalid selection for LOD cloning procedure" lod_clone_proc
  438. )
  439. case damage_clone_proc of
  440. (
  441. 1: damage_proc = copy
  442. 2: damage_proc = instance
  443. 3: damage_proc = reference
  444. default: throw "Invalid selection for Damage cloning procedure" damage_clone_proc
  445. )
  446. -- Create the LOD models
  447. createLOD count:lod_count offset:[lod_offset,0,0] clone_by:lod_proc
  448. -- Create the Damage models
  449. createDamage count:damage_count offset:[0,damage_offset,0] clone_by:damage_proc
  450. )
  451. return OK
  452. )
  453. -- The macro script definition for the toolbar button.
  454. macroScript LOD_And_Damage_Setup
  455. category:"Westwood Scripts"
  456. buttonText:"LOD and Damage Setup"
  457. toolTip:"LOD and Damage Setup"
  458. icon:#("GameTools", 1)
  459. (
  460. if objects.count == 0 then
  461. (
  462. messageBox("There are no objects in the current scene. Load " + \
  463. "a scene first.")
  464. )
  465. else
  466. (
  467. sceneSetup()
  468. )
  469. )
  470. -- The macro script definition for the "create origin" toolbar
  471. -- button. This will create a new origin object centered at 0,0,0
  472. -- assign all objects a ".00" extension, and link all top-level
  473. -- objects to the new origin.
  474. macroScript Create_Origin
  475. category:"Westwood Scripts"
  476. buttonText:"Create Origin"
  477. toolTip:"Create Origin"
  478. icon:#("Helpers", 2)
  479. (
  480. if objects.count == 0 then
  481. (
  482. messageBox "An origin is not useful in an empty scene."
  483. return OK
  484. )
  485. if $'Origin.00' == undefined then
  486. (
  487. local origin = SS_create_origin()
  488. SS_build_hierarchy origin
  489. print(origin.name + " created.")
  490. )
  491. else
  492. (
  493. messageBox "Origin.00 already exists"
  494. )
  495. )