• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

Doodad2Model Merger

Status
Not open for further replies.
Level 8
Joined
Jul 24, 2006
Messages
157

Doodad2Model Merger

I made this tool for single player maps with first person camera.
Here is my tutorial for the FPS Camera: FPS Camera with SharpCraft
attachment.php


This tool can open a map and merge doodads in a given rect to a single model.
This is how rect coordinates are displaced in the editor:
attachment.php


Merging means that even the geosets with the same material a merged.
Why should I merge doodads into a model?

Pro:

-Performance Increases
-Less visibility bugs with first person camera

Con:

-Large map size


What does this tool merge?

-Doodads
-*new* Doodad Colors

What is not supported?

-Animations
-Destructables

Ignore List

An additional feature is the ignorelist.txt, here you can write down names of models seperated with a linebreak.
This doodads are ignored when you use the merger.

Example

Here is a robot I merged with this tool (created by Corexx):

attachment.php

attachment.php


Download

And here is the 0.2 alpha of the tool: War3DoodadMerger v0.2
Keep in mind that it is unfinished and can contain bugs.
 

Attachments

  • Robot.jpg
    Robot.jpg
    107.4 KB · Views: 577
  • RobotMagos.png
    RobotMagos.png
    217.6 KB · Views: 563
  • Coordinates.jpg
    Coordinates.jpg
    24.1 KB · Views: 530
  • War3DoodadMerger_0.1b.rar
    3.3 MB · Views: 95
  • DoodadMergerGUI.png
    DoodadMergerGUI.png
    11.8 KB · Views: 303
  • War3DoodadMerger_0.2.rar
    3.3 MB · Views: 241
Last edited:
Level 8
Joined
Jul 24, 2006
Messages
157
Does it remove "inner" polys? Like, polygons that arent visible because they are covered by other doodads.
No, I dont know an algorithm to do that efficently and sometime you can see the polygons from a view under a model and this would not be possible in game.
But in my tests the performance was increased simply by the fact that we get less doodads with less materials and less geometries.
 
Level 8
Joined
Jul 24, 2006
Messages
157
I wonder how big is the file?
The robot consists of 464 doodads, the merged model has a size of 4MB (squished in a map 950KB)
Thats why I said it is for single player where map size doesnt matter that much.

I've got a question though. You write: "I made this tool for single player maps with first person camera.", does this mean that it's redundant to use in maps with standard camera view?
With a standard camera view you can only have performance problems when you mass doodads and add the function to zoom out with your camera.
 
Level 14
Joined
Nov 17, 2010
Messages
1,266
I think it's a pretty great idea. Imagine making custom buildings or stairways or statues out of many doodads and then merging them into one. I could definitely use this.
 
Level 8
Joined
Jul 24, 2006
Messages
157
Seems cool to me. If I were you, I'd create some benchmarks. It'll better gauge how profitable this tool can be.

Spam an area with doodads, measure fps. Convert using tool, import, and measure fps. I'm curious of what the impact will be.

I already did.
I created a field of 2200 high poly sunflowers, as a result I only got 30fps when they were all on the screen.
Then I merged a chunk of 187 sunflowers into a sunflower model.
After this I could create a field 4-times as large as the original one and I still got full 60fps. (60fps is wc3 maximum)
 
I already did.
I created a field of 2200 high poly sunflowers, as a result I only got 30fps when they were all on the screen.
Then I merged a chunk of 187 sunflowers into a sunflower model.
After this I could create a field 4-times as large as the original one and I still got full 60fps. (60fps is wc3 maximum)

Awesome. Perhaps you should mention that in the first post, I'm sure it'll make the tool more enticing.
 
Level 1
Joined
Dec 15, 2012
Messages
6
Usefull, maybe for creating custom campaign screen ... I'm just curious how it work more deeply, if is it capable to recognize doodads and associated models, than is possible make tool that recognize used doodads and delete unused from map/archive (but i quess some mapoptimizer tool already doing that stuff) ...
however, warcraft is already past his prime, but support tools have never been more ...
 
No, I dont know an algorithm to do that efficently and sometime you can see the polygons from a view under a model and this would not be possible in game.
But in my tests the performance was increased simply by the fact that we get less doodads with less materials and less geometries.

Try looking into raytracing algorithms. You could try sending millions of rays from a bounding sphere and picking the triangles that they hit, then simplify the mesh based on that. It's costy and could take sometime but will do the job in a desireable time. You could simplify the colision test process using octrees.

here's the code for octrees I use for NeoDex's vertex optimizer:
Code:
(
	fn cond a = false
    struct _Octree
    (
        data = #(),
        dataSet,
        origin,
        halfDimension,
        level,
        maxLevel,
        maxSize,
        children,
		--conditionFunc = cond,
		this,
        fn create level origin halfDimension dataSet maxLevel:0x7FFFFFF maxSize:0x7FFFFFF =
        (
            local o = _Octree ()
            o.level = level
            o.origin = origin
            o.halfDimension = halfDimension
            o.dataSet = dataSet
            o.maxLevel = maxLevel
            o.maxSize = maxSize
			o.this = o
			o
        ),
        fn isLeafNode =
        (
           children == undefined
        ),
        fn getOctant p = 
		(
			local oct = 0
			if (p.x >= origin.x) then oct = bit.or oct 4
			if (p.y >= origin.y) then oct = bit.or oct 2
			if (p.z >= origin.z) then oct = bit.or oct 1
			(oct + 1)
		),
        fn insertPoint p =
        (
            if (isLeafNode()) then 
            (
				if (data.count < maxSize or level >= maxLevel) then
					append data p
				else
                (
                    children = #()
                    children[8] = undefined
					-- Split the current node and create new empty trees for each
					-- child octant.
					for i = 0 to 7 do 
                    (
						-- Compute new bounding box for this child
						
                        local n = [0.5,0.5,0.5]
                        if (bit.get i 3) then n.x = -n.x
                        if (bit.get i 2) then n.y = -n.y
                        if (bit.get i 1) then n.z = -n.z
                        local newOrigin = origin - halfDimension*n
						children[i+1] = _Octree.create (level+1) newOrigin (halfDimension*0.5) dataSet maxLevel:maxLevel maxSize:maxSize
						--children[i+1].conditionFunc = conditionFunc
					)
                    
					-- Re-insert the old point, and insert this new point
					-- (We wouldn't need to insert from the root, because we already
					-- know it's guaranteed to be in this section of the tree)
                    for d in data do
                        children[(getOctant dataSet[d])].insertPoint d
                    data = undefined
					children[(getOctant dataSet)].insertPoint p
				)
			)
            else 
            (
				-- We are at an interior node. Insert recursively into the 
				-- appropriate child octant
				children[(getOctant dataSet)].insertPoint p
			)
        ),
		fn getBlocks =
		(
			if (isLeafNode()) then 
				#(data)
			else
			(
				local result = #()
				for c in children do
				(
					local unitBlock = c.getBlocks ()
					if (unitBlock[1].count != 0) then
						join result unitBlock
				)
				result
			)
		),
		fn printInfo =
		(
			format "Octree Level %\n" level to:listener
			format "  Origin %\n" origin to:listener
			format "  HalfDim %\n" halfDimension to:listener
			if isLeafNode() then
			(
				format "  Leaf Node\n" to:listener
				for d in data do
					format "  %\n" dataSet[d] to:listener
			)
			else
			(
				format "  Parent Node\n" to:listener
				for c in children do
					c.printInfo ()
			)
		)
    )
    struct _Quadtree
    (
        data = #(),
        dataSet,
        origin,
        halfDimension,
        level,
        maxLevel,
        maxSize,
        children,
		conditionFunc = cond,
		this,
        fn create level origin halfDimension dataSet maxLevel:0x7FFFFFF maxSize:0x7FFFFFF =
        (
            local o = _Quadtree ()
            o.level = level
            o.origin = origin
            o.halfDimension = halfDimension
            o.dataSet = dataSet
            o.maxLevel = maxLevel
            o.maxSize = maxSize
			o.this = o
			o
        ),
        fn isLeafNode =
        (
           children == undefined
        ),
        fn getQuadrant p = 
		(
			local oct = 0
			if (p.x >= origin.x) then oct = bit.or oct 2
			if (p.y >= origin.y) then oct = bit.or oct 1
			(oct + 1)
		),
        fn insertPoint p =
        (
            if (isLeafNode()) then 
            (
				if (conditionFunc this) or (data.count < maxSize or level >= maxLevel) then
					append data p
				else
                (
                    children = #()
                    children[4] = undefined
					-- Split the current node and create new empty trees for each
					-- child octant.
					for i = 0 to 3 do 
                    (
						-- Compute new bounding box for this child
						
                        local n = [0.5,0.5]
                        if (bit.get i 2) then n.x = -n.x
                        if (bit.get i 1) then n.y = -n.y
                        local newOrigin = origin - halfDimension*n
						children[i+1] = _Quadtree.create (level+1) newOrigin (halfDimension*0.5) dataSet maxLevel:maxLevel maxSize:maxSize
						children[i+1].conditionFunc = conditionFunc
					)
                    
					-- Re-insert the old point, and insert this new point
					-- (We wouldn't need to insert from the root, because we already
					-- know it's guaranteed to be in this section of the tree)
                    for d in data do
                        children[(getQuadrant dataSet[d])].insertPoint d
                    data = undefined
					children[(getQuadrant dataSet)].insertPoint p
				)
			)
            else 
            (
				-- We are at an interior node. Insert recursively into the 
				-- appropriate child octant
				children[(getQuadrant dataSet)].insertPoint p
			)
        ),
		fn getBlocks =
		(
			if (isLeafNode()) then 
				#(data)
			else
			(
				local result = #()
				for c in children do
				(
					local unitBlock = c.getBlocks ()
					if (unitBlock[1].count != 0) then
						join result unitBlock
				)
				result
			)
		),
		fn printInfo =
		(
			format "Quadtree Level %\n" level to:listener
			format "  Origin %\n" origin to:listener
			format "  HalfDim %\n" halfDimension to:listener
			if isLeafNode() then
			(
				format "  Leaf Node\n" to:listener
				for d in data do
					format "  %\n" dataSet[d] to:listener
			)
			else
			(
				format "  Parent Node\n" to:listener
				for c in children do
					c.printInfo ()
			)
		)
    )
    global Octree
    fn Octree dataSet minimum maximum maxLevel:0x7FFFFFF maxSize:0x7FFFFFF =
    (
        local center = (maximum - minimum)*0.5
        _Octree.create 0 center (maximum - center) dataSet maxLevel:maxLevel maxSize:maxSize
    )
    global Quadtree
    fn Quadtree dataSet minimum maximum maxLevel:0x7FFFFFF maxSize:0x7FFFFFF =
    (
        local center = (maximum - minimum)*0.5
        _Quadtree.create 0 center (maximum - center) dataSet maxLevel:maxLevel maxSize:maxSize
    )
)

fn generateTestSet =
(
	local a = #()
	for i = 1 to 10000 do
	(
		append a [random 0.0 100.0, random 0.0 100.0, random 0.0 100.0]
	)
	a
)

fn myCondition this =
(
	this.halfDimension.x < 2.5
)

fn Test =
(
	local s = generateTestSet ()
	local o = Octree s [0,0,0] [100,100,100] maxSize:10
	o.conditionFunc = myCondition
	for i = 1 to s.count do
		o.insertPoint i
	try
		o.getBlocks ()
	catch
		print (getCurrentException ())
)

To do a collision test, you must look at which blocks the ray passes by, for each of those blocks, you must look at the vertices that are inside and map from vertex to triangle and check if the ray hits that triangle.

I suggest you first create a dataSet which maps from vertex to owned triangles.

When testing collisions keep a hashtable to write down tested triangles, you don't want to test a triangle more than once.

A good approach for generating rays is that you generate rays based on the octree's distribution. for each leafnode you should project it's inside with a ray coming out of the bounding sphere from 12 possible directions. This should be enough to figure out collision. It will make your algorithm depend on the number of octrees leaves made. if you allow semiunlimited levels (maxLevels 0x0000ffff) and a maxsize of 3. Another simplification is to only generate rays for leafoctrees with more than 1 vertex.

Another aproach is to make rays every set of curvature distance that point to the center or a random point within the bounding sphere. Then make the rays bounce on colliding with a triangle. Set a limit of ray bounces and it should touch all or most of the seeable triangles.
 
Last edited:
Can't you get a decent enough approximation by sending rays from vertices according to the polygon's normal?
That would be just 3*N, where N is the number of vertices.

Now that I think about it, thta's an even greater idea as far as you send the rays from each vertex with the generated triangle normal. If the rays hit the bounding sphere before another triangle then the face will be visible.
 
Level 29
Joined
Jul 29, 2007
Messages
5,174
Now that I think about it, thta's an even greater idea as far as you send the rays from each vertex with the generated triangle normal. If the rays hit the bounding sphere before another triangle then the face will be visible.

It's not as accurate of course, but I wonder how much deviation there would be.
It could also be interesting to make a configurable interpolation between the vertices, e.g. 0 would be only the vertices [0, 1], 1 would be [0, 0.5, 1], and so on.
 
Last edited:
It's not as accurate of course, but I wonder how much deviation there would be.
It could also be interesting to make a configurable interpolation between the vertices, e.g. 0 would be only the vertices [0, 1], 1 would be [0, 0.5, 1], and so on.

Well the general idea would be to simulate photon mapping in order to capture the faces which could recieve light.

Blender appears to have an option which can get invisible faces. probably they use a more math based method rather than a simulation.
 
So basicly, we now have a tool to "mudbox" high poly models from small elements?

So basicly, if one were to take a single brick model, you are able to doodad-art a complete house and then weld it into a single model? That's awesome! Imagine the possibilities!

- people are able to create "terrain patches" that can be shared and easily imported into any map
- using, f.ex. talavaj's modular manor kit, people are able to create basicly any complexity of buildings
- friggin' animated high poly battleships and tanks as units!
- friggin' animated giants!

Unfortunately, this is not feasable for multiplayer maps due to filesize.
 
Status
Not open for further replies.
Top