Merge lp:~widelands-dev/widelands/full_texture_atlas into lp:widelands
- full_texture_atlas
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 7704 |
Proposed branch: | lp:~widelands-dev/widelands/full_texture_atlas |
Merge into: | lp:widelands |
Diff against target: |
593 lines (+305/-110) 7 files modified
src/graphic/image_cache.cc (+0/-33) src/graphic/image_cache.h (+0/-4) src/graphic/make_texture_atlas_main.cc (+196/-23) src/graphic/texture_atlas.cc (+56/-38) src/graphic/texture_atlas.h (+38/-5) src/logic/map_objects/tribes/tribes.cc (+8/-4) src/logic/map_objects/world/world.cc (+7/-3) |
To merge this branch: | bzr merge lp:~widelands-dev/widelands/full_texture_atlas |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
GunChleoc | Approve | ||
Review via email: mp+281909@code.launchpad.net |
Commit message
- Support maximum dimensions when baking texture atlases.
- Change wl_make_
Description of the change
This branch should not bring any functional change to the game at all. The focus here is to make the binary wl_make_
Testers:
Please do the following after building the branch. Go into the root directory of the bzr repository
and run the binary, for example this should work if you use compile.sh:
build/src/
This should generate some output*.png and a output.lua.
I am mostly interested in two things:
1) how long does it take to run this command
2) What is your maximum texture size? The program prints it at the very beginning.
Potential Impact:
Right now, Widelands has 31919 images. They are loaded on-demand from disk which is bad, because it makes the game stutter if you have a slow HD. It also makes the code ugly, because not all images are treated the same. They are also loaded into a separate texture each which leads to very many OpenGL context switches for each frame during rendering.
A 3-5 year old graphics card should be able to deal with 8192x8192 textures, no prob. Given that resolution, all our images can fit into 2 OpenGL textures when packed. These two PNGs could be loaded very quickly on game startup and we will have no loading stutter and much less context switches. This will be the biggest performance gain in rendering ever.
Open questions:
For the followup branch there are two possible implementations:
1) build a texture atlas on startup and cache it into ~/.wideland/cache, so that it does not need to be rebuild for the particular Widelands version. On my system, building the atlases takes ~30seconds, I think it is acceptable for the first startup to take that long.
Pros/Cons:
+ Nearly no change in deployment and development for Widelands devs.
- Whenever you commit, your next Widelands startup will be slow because the version has changed and therefor the atlases will be rebuild.
- First Widelands start will be slow for new users. First impression are important though!
- Building the cache is a code path that will not be often executed, so bugs might slip in more easily.
- Graphic devs need to manually nuke the cache when they change a graphic, since the shipped images will not be used if the cache is there.
2) build texture atlases offline for various resolutions and only ship these in the release version.
- Graphic dev becomes harder, because graphic devs need to bake the texture atlases before starting the game every time they change a graphic to see them in the game.
- released versions can no longer tweak a graphic or add new ones, since only the texture atlases are ever used and they are hard to understand for a human.
+ The release becomes way smaller, since we would only ship a few images instead of thousands. This could even be pushed further, by 'baking' all of our resources into a simpler package that is easier to load on start - similar to how the current wl_make_
Which one do you like better, 1 or 2?
TiborB (tiborb95) wrote : | # |
Tino (tino79) wrote : | # |
Info:
Graphics: Try to set Videomode 1x1
Graphics: OpenGL: Version "4.3.0 - Build 10.18.14.4139"
Graphics: OpenGL: Double buffering enabled
Graphics: OpenGL: Max texture size: 16384
Runtime to do a wl_make_
Hardware is Lenovo x240 Notebook with an Intel Core i7-4600U.
My vote goes to variant 2. On windows system with AV software there should be a performance gain because fewer files have to be scanned.
I don't think 60 seconds are a blocker for Graphic devs: How long does it take to e.g. render a new animation with Blender? Does this both then end up too long?
What about building a small testing app which just renders/animates single widelands animations for a first visual check?
bunnybot (widelandsofficial) wrote : | # |
Hi, I am bunnybot (https:/
I am keeping the source branch lp:~widelands-dev/widelands/full_texture_atlas mirrored to https:/
You can give me commands by starting a line with @bunnybot <command>. I understand:
merge: Merges the source branch into the target branch, closing the merge proposal. I will use the proposed commit message if it is set.
kaputtnik (franku) wrote : | # |
Is the atlas used in this branch?
Run time on my computer (AMD x2 5200+):
time ./build/
Graphics: Try to set Videomode 1x1
Graphics: OpenGL: Version "3.0 Mesa 11.1.0"
Graphics: OpenGL: Double buffering enabled
Graphics: OpenGL: Max texture size: 16384
**** GRAPHICS REPORT ****
VIDEO DRIVER x11
pixel fmt 370546692
size 640 480
**** END GRAPHICS REPORT ****
[libpng warnings]
real 2m17.223s
user 1m27.270s
sys 0m40.343s
2 Minutes.
Resulting file size of output_00.png: 33M (34347800 byte)
TiborB (tiborb95) wrote : | # |
$ time ./wl_make_
Graphics: Try to set Videomode 1x1
Graphics: OpenGL: Version "3.0 Mesa 11.0.7"
Graphics: OpenGL: Double buffering enabled
Graphics: OpenGL: Max texture size: 8192
**** GRAPHICS REPORT ****
VIDEO DRIVER x11
pixel fmt 370546692
size 640 480
**** END GRAPHICS REPORT ****
libpng warning: iCCP: known incorrect sRGB profile #REPEATED couple of times
real 1m14.118s
user 0m41.267s
sys 0m5.100s
$ ls -al output*
-rw-r--r-- 1 tibor tibor 33135351 jan 7 21:54 output_00.png
-rw-r--r-- 1 tibor tibor 34348444 jan 7 21:54 output_01.png
-rw-r--r-- 1 tibor tibor 4961263 jan 7 21:54 output.lua
-------
$ time ./wl_make_
........
real 0m33.660s
user 0m32.740s
sys 0m0.730s
ls -al output*
-rw-r--r-- 1 tibor tibor 440653 jan 7 21:58 output_00.png
-rw-r--r-- 1 tibor tibor 4983963 jan 7 21:58 output.lua
bunnybot (widelandsofficial) wrote : | # |
Travis build 220 has changed state to: errored. Details: https:/
kaputtnik (franku) wrote : | # |
The second is for graphic devs very hard. If one changes graphics, he has to put the atlas into a branch and upload for testing? This may cause heavy upload traffic. What is meant with "various resolutions"? Will this restrict the resolution options? F.e. no 800x600 resolution anymore?
The more i think of it the first one would be better. The main reason against this, is that the atlas has to be build on the users computer and needs some time... May there could be some kind of slideshow and/or descriptions of the game to bridge the time for the user while in the background the atlas is made?
SirVer (sirver) wrote : | # |
Update: If you want te test the usage of the texture atlas in game, use this branch:
https:/
After you generated the texture atlas, you have to move the files into a directory called cache:
$ build/src/
$ mkdir cache
$ mv output* cache
Then start Widelands as usual.
> Can the building be part of installation process?
No, since it requires an OpenGL context to build the texture atlas. Therefore, a GUI is needed.
> I dont understand that stuff fully, but an user can pick various resolutions + windowed/fullscreen mode on his/her PC - no problem with this?
No, no problem with that.
> If one changes graphics, he has to put the atlas into a branch and upload for testing?
No, a graphic dev would need to run the program like described in this merge request - they could do that locally. The problem is that we'd need to build texture atlases at various sizes: 16384, 8192, 4096, 2048 and check all images in. If a dev changes one of the source images, all of these atlases would change again - this would yield huge changes to the bzr repo (since the images are big, 50MB).
> Will this restrict the resolution options? F.e. no 800x600 resolution anymore?
No, I meant the max texture resolution of graphics cards. The game will not loose any features, just gain performance.
> May there could be some kind of slideshow and/or descriptions of the game to bridge the time for the user while in the background the atlas is made?
That is hard - since all graphics need to go into the atlas. We could probably show a still image or some text, maybe also a progress bar.
bunnybot (widelandsofficial) wrote : | # |
Travis build 220 has changed state to: passed. Details: https:/
kaputtnik (franku) wrote : | # |
> > May there could be some kind of slideshow and/or descriptions of the game to
> bridge the time for the user while in the background the atlas is made?
>
> That is hard - since all graphics need to go into the atlas. We could probably
> show a still image or some text, maybe also a progress bar.
Isn't it possible to exclude a folder with images from being included into the atlas?
We should consider to check all images if they are really used in the game or if we could use one image in different places. F.e. the world/resources
SirVer (sirver) wrote : | # |
> Isn't it possible to exclude a folder with images from being included into the atlas?
yes. in fact, big pictures are already not included into the atlas, so there will still be a few standalone textures.
> Of course these pics are all very smal...
So they do not really matter that much anyways. We could get perfect optimization by compressing the texture atlas too, then duplicated images would be removed anyways.
kaputtnik (franku) wrote : | # |
Values from another maschine:
Intel(R) Core(TM) i5-2410M CPU @ 2.30GHz
time ./build/
Graphics: Try to set Videomode 1x1
Graphics: OpenGL: Version "3.0 Mesa 11.1.0"
Graphics: OpenGL: Double buffering enabled
Graphics: OpenGL: Max texture size: 8192
[...]
real 1m16.709s
user 1m2.800s
sys 0m10.137s
ls -l outpu*
-rw-r--r-- 1 kaputtnik kaputtnik 4961263 8. Jan 15:36 output.lua
-rw-r--r-- 1 kaputtnik kaputtnik 33675230 8. Jan 15:36 output_00.png
-rw-r--r-- 1 kaputtnik kaputtnik 35077200 8. Jan 15:36 output_01.png
This laptop uses only integrated GPU. I will try to activate the video card and test again.
GunChleoc (gunchleoc) wrote : | # |
I like the idea of adding a special loading screen when Widelands is first run - e.g. the text could read "Optimizing Widelands images for first usage" or some such, so the user will know that they won't have to wait for this in the future.
We should then build the texture atlas only for the current resolution + fullscreen mode - the other resolutions could be calculated when the user switches the resolution, with a similar loading screen.
SirVer (sirver) wrote : | # |
> I like the idea of adding a special loading screen when Widelands is first run - e.g. the text could read "Optimizing Widelands images for first usage" or some such, so the user will know that they won't have to wait for this in the future.
I'll look into that. I think that should be possible.
> We should then build the texture atlas only for the current resolution + fullscreen mode - the other resolutions could be calculated when the user switches the resolution, with a similar loading screen.
The in-game resolution and fullscreen/window is of no matter. We need to build the texture atlas for the max gl texture resolution of the graphics card we are running on. And of course for the current Widelands version - if the version change, graphics might have changed too, so we need to rebuild the atlas.
There have been many comments on this branch now. Can I merge it? The next branch will move the functionality of the binary into Widelands itself, so the code will live no, but the binary will go away soon.
kaputtnik (franku) wrote : | # |
> There have been many comments on this branch now. Can I merge it? The next
> branch will move the functionality of the binary into Widelands itself, so the
> code will live no, but the binary will go away soon.
Nothing against your proposal, but we have seen that with every merge the last few weeks new bugs come up. There are great enhancements to widelands and until now, and i really want to have a feature freeze at current state. It is a lot of stuff to solve the already known bugs and i fear with every new feature the amount of bugs gets greater and a release gets far away.
But as said, i know you're a great programmer and your work would enhance widelands a lot. On the other side i have never heard that widelands is really slow (except long play games). So i think your proposal fixes problems that are not really there.
The changes which comes up with the render queue needs some fixing to images i think, Some of them looks now very sharp which is on side nice, on the other side some needs a bit tweeking, because they are too sharp. F.e. desert water has some sparkle in it, which now looks not very good imho. Changing those images and always make a new atlas sounds not good to me.
I think we should test the full texture atlas at a whole, f.e. with a "first run screen" and address it to build 20.
But in the end it's your decision to merge. You are the maintainer of widelands and i believe you know what you are doing :-)
GunChleoc (gunchleoc) wrote : | # |
I had another idea regarding the loading screen: ui_fsmenu doesn't really need a performance enhancement, so we could not use the texture atlas in the fullscreen menus and generate it in the background, then finish loading it in the game/editor loading screens if necessary. I don't know the consequences for bug hunting/fixing of this though.
Another argument in favor of solution 1: modders can change images more easily. The texture atlas could also have a hash on file number/sizes/dates (maybe remember the file date on the newest file for each folder only?), so if a graphic dev changes something, the texture atlas is recalculated. Graphics devs will still need to wait, but they won't need to manually nuke the cache every time.
kaputtnik (franku) wrote : | # |
Another question:
I use an extra datadir folder which contains some special images and that i use for testing purposes. What happens to the --datadir option when this get merged? Could i further use it for testing images?
SirVer (sirver) wrote : | # |
[franku's concern about need for this change]
I play Widelands in fullscreen at 3440 x 1440. At that resolution, the game needs 90% of the single CPU it uses just to draw the screen. I want to add zooming in and out into Widelands, but that needs that performance even worse. On my wifes computer, Widelands noticeable stutters whenever the game loads a graphic from disk. I have been working on these performance improvements for over a year now - I think they are very important and I want to have them in build 19.
I understand your concerns about feature freeze and new bugs, but this is really important to me.
[background creation of texture atlas]
You want the mouse cursor in the texture atlas though, so that does not really work out. Also background generation is tricky business - what if some users jumps quickly through the menus and you need the atlas before you are ready. I'd rather not deal with these headaches.
[hash on the atlas]
You'd need to remember the hashes somewhere (in the lua file probably) and traverse all widelands data directories for all pictures and stat them on every game start - which is pretty slow already. Not as slow as building the cache, but probably ten seconds or so on a non Solid state disk. It would be an alternative to consider if the build_info() is too coarse grained.
> I use an extra datadir folder which contains some special images and that i use for testing purposes. What happens to the --datadir option when this get merged? Could i further use it for testing images?
Yes, but if you change an image, you'd need to manually run rm -rf ~/.widelands/cache, otherwise your new images would not be used.
TiborB (tiborb95) wrote : | # |
I agree that this should be changed now - before build 19. This is performance improvement and game needs it badly.
But I also agree that we need to declare feature freeze ASAP and focus on feature freeze.
@SirVer, what else do you want to introduce before b19? I think your changes are last big changes before we enter the feature freeze.
What do you think?
SirVer (sirver) wrote : | # |
> @SirVer, what else do you want to introduce before b19? I think your changes are last big changes before we enter the feature freeze.
I have one more smaller refactoring out that I want to get in [1]. I wanted to build zooming into the game (that was, what got me started on the whole GL improvements road), but I agree that this can wait till after build 19, so I delay that.
Once these branches are in, I'll focus on getting bugs fixed for b19 and I am fine with feature freezing. There are functional changes that need to happen but which I consider bug fixing - for example updating Eris, our Lua persistent library in third_party/ is a necessity to not break savegames in the future again.
[1] https:/
kaputtnik (franku) wrote : | # |
SirVer, i didn't know that you put so much time into this. And zooming would be a very nice thing to have, even if it comes in build 20 :-)
As i understand, merging just this branch is only the preparation for implementing the whole stuff and wouldn't change anything for now. So i am fine with merging.
Is there a decision which implementation (first or second) would be used? If we have a "first load for optimizing widelands" screen, would it be possible to have a slideshow/diashow in there? So we could may use some of the images from https:/
SirVer (sirver) wrote : | # |
> As i understand, merging just this branch is only the preparation for implementing the whole stuff and wouldn't change anything for now. So i am fine with merging.
The full implementation is https:/
I have a lot of testing feedback for this, but no code review comments. Could I get a lgtm for the code before merging?
GunChleoc (gunchleoc) wrote : | # |
I will try to look at the code shoonish.
Regarding a feature freeze, there are 2 things that still need doing on my end:
1. Font renderer switchover: We have 1 wordwrap bug that needs fixing (https:/
2. Savegame compatibility is not done. We need to fix that up, because we want to keep compatibility again starting from builg 19 - we need to identify the packages that we can ignore with map loading and not load them (implement https:/
I would also like the editor help in. It's not 100% slick (e.g. no resources help yet), but it does already contain some very valuable information and shouldn't introduce new bugs https:/
TiborB (tiborb95) wrote : | # |
My understanding of feature freeze is that we will finish all opened work (f.e. branches waiting for merges, or ones that are really follow ups of what is opened now) but no brand new and unrelated changes/
F.e. I am not going to make any changes to AI, there is only one branch waiting for merge review, this is what to be nice to have merged before release...
GunChleoc (gunchleoc) wrote : | # |
Some small nits, otherwise LGTM
SirVer (sirver) wrote : | # |
Adressed all comments and merged trunk. If everything passes on travis I'll merge this.
SirVer (sirver) wrote : | # |
@bunnybot merge
Preview Diff
1 | === modified file 'src/graphic/image_cache.cc' |
2 | --- src/graphic/image_cache.cc 2016-01-12 07:00:44 +0000 |
3 | +++ src/graphic/image_cache.cc 2016-01-12 21:27:11 +0000 |
4 | @@ -31,12 +31,6 @@ |
5 | #include "graphic/image_io.h" |
6 | #include "graphic/texture.h" |
7 | |
8 | -namespace { |
9 | - |
10 | -constexpr int kBiggestAreaForCompactification = 250 * 250; |
11 | - |
12 | -} // namespace |
13 | - |
14 | ImageCache::ProxyImage::ProxyImage(std::unique_ptr<const Image> original_image) |
15 | : image_(std::move(original_image)) { |
16 | } |
17 | @@ -87,30 +81,3 @@ |
18 | } |
19 | return it->second.get(); |
20 | } |
21 | - |
22 | -void ImageCache::compactify() { |
23 | - TextureAtlas texture_atlas; |
24 | - |
25 | - std::vector<std::string> hashes; |
26 | - for (const auto& pair : images_) { |
27 | - const auto& image = pair.second->image(); |
28 | - if (image.width() * image.height() > kBiggestAreaForCompactification) { |
29 | - continue; |
30 | - } |
31 | - |
32 | - texture_atlas.add(image); |
33 | - hashes.push_back(pair.first); |
34 | - } |
35 | - |
36 | - std::vector<std::unique_ptr<Texture>> new_textures; |
37 | - |
38 | - // TODO(sirver): Limit the size of the texture atlas to a max GL texture |
39 | - // size. This might return more than one packed image. Make sure that the |
40 | - // code works also for small max texture sizes. |
41 | - texture_atlases_.emplace_back(texture_atlas.pack(&new_textures)); |
42 | - |
43 | - assert(new_textures.size() == hashes.size()); |
44 | - for (size_t i = 0; i < hashes.size(); ++i) { |
45 | - images_[hashes[i]]->set_image(std::move(new_textures[i])); |
46 | - } |
47 | -} |
48 | |
49 | === modified file 'src/graphic/image_cache.h' |
50 | --- src/graphic/image_cache.h 2016-01-04 20:54:08 +0000 |
51 | +++ src/graphic/image_cache.h 2016-01-12 21:27:11 +0000 |
52 | @@ -55,10 +55,6 @@ |
53 | // Returns true if the given hash is stored in the cache. |
54 | bool has(const std::string& hash) const; |
55 | |
56 | - // For debug only: Takes all images that are in the ImageCache right now and |
57 | - // puts them into one huge texture atlas. |
58 | - void compactify(); |
59 | - |
60 | private: |
61 | // We return a wrapped Image so that we can swap out the pointer to the |
62 | // image under our user. This can happen when we move an Image from a stand |
63 | |
64 | === modified file 'src/graphic/make_texture_atlas_main.cc' |
65 | --- src/graphic/make_texture_atlas_main.cc 2016-01-04 20:54:08 +0000 |
66 | +++ src/graphic/make_texture_atlas_main.cc 2016-01-12 21:27:11 +0000 |
67 | @@ -22,10 +22,12 @@ |
68 | #include <memory> |
69 | #include <set> |
70 | #include <string> |
71 | +#include <unordered_set> |
72 | #include <vector> |
73 | |
74 | #include <SDL.h> |
75 | #include <boost/algorithm/string/predicate.hpp> |
76 | +#include <boost/format.hpp> |
77 | |
78 | #undef main // No, we do not want SDL_main |
79 | |
80 | @@ -41,15 +43,39 @@ |
81 | |
82 | namespace { |
83 | |
84 | +// This is chosen so that all graphics for tribes are still well inside this |
85 | +// threshold, but not background pictures. |
86 | +constexpr int kMaxAreaForTextureAtlas = 240 * 240; |
87 | + |
88 | +constexpr int kMinimumSizeForTextures = 2048; |
89 | + |
90 | +// An image can either be Type::kPacked inside a texture atlas, in which case |
91 | +// we need to keep track which one and where inside of that one. It can also be |
92 | +// Type::kUnpacked if it is to be loaded from disk. |
93 | +struct PackInfo { |
94 | + enum class Type { |
95 | + kUnpacked, |
96 | + kPacked, |
97 | + }; |
98 | + |
99 | + Type type; |
100 | + int texture_atlas; |
101 | + Rect rect; |
102 | +}; |
103 | + |
104 | int parse_arguments( |
105 | - int argc, char** argv, std::string* input_directory) |
106 | + int argc, char** argv, int* max_size) |
107 | { |
108 | if (argc < 2) { |
109 | - std::cout << "Usage: wl_make_texture_atlas <input directory>" << std::endl << std::endl |
110 | + std::cout << "Usage: wl_make_texture_atlas [max_size]" << std::endl << std::endl |
111 | << "Will write output.png in the current directory." << std::endl; |
112 | return 1; |
113 | } |
114 | - *input_directory = argv[1]; |
115 | + *max_size = atoi(argv[1]); |
116 | + if (*max_size < kMinimumSizeForTextures) { |
117 | + std::cout << "Widelands requires at least 2048 for the smallest texture size." << std::endl; |
118 | + return 1; |
119 | + } |
120 | return 0; |
121 | } |
122 | |
123 | @@ -62,11 +88,121 @@ |
124 | g_gr = new Graphic(1, 1, false); |
125 | } |
126 | |
127 | +// Returns true if 'filename' ends with an image extension. |
128 | +bool is_image(const std::string& filename) { |
129 | + return boost::ends_with(filename, ".png") || boost::ends_with(filename, ".jpg"); |
130 | +} |
131 | + |
132 | +// Recursively adds all images in 'directory' to 'ordered_images' and |
133 | +// 'handled_images' for which 'predicate' returns true. We keep track of the |
134 | +// images twice because we want to make sure that some end up in the same |
135 | +// (first) texture atlas, so we add them first and we use the set to know that |
136 | +// we already added an image. |
137 | +void find_images(const std::string& directory, |
138 | + std::unordered_set<std::string>* images, |
139 | + std::vector<std::string>* ordered_images) { |
140 | + for (const std::string& filename : g_fs->list_directory(directory)) { |
141 | + if (g_fs->is_directory(filename)) { |
142 | + find_images(filename, images, ordered_images); |
143 | + continue; |
144 | + } |
145 | + if (is_image(filename) && !images->count(filename)) { |
146 | + images->insert(filename); |
147 | + ordered_images->push_back(filename); |
148 | + } |
149 | + } |
150 | +} |
151 | + |
152 | +void dump_result(const std::map<std::string, PackInfo>& pack_info, |
153 | + std::vector<std::unique_ptr<Texture>>* texture_atlases, |
154 | + FileSystem* fs) { |
155 | + |
156 | + for (size_t i = 0; i < texture_atlases->size(); ++i) { |
157 | + std::unique_ptr<StreamWrite> sw( |
158 | + fs->open_stream_write((boost::format("output_%02i.png") % i).str())); |
159 | + save_to_png(texture_atlases->at(i).get(), sw.get(), ColorType::RGBA); |
160 | + } |
161 | + |
162 | + { |
163 | + std::unique_ptr<StreamWrite> sw(fs->open_stream_write("output.lua")); |
164 | + sw->text("return {\n"); |
165 | + for (const auto& pair : pack_info) { |
166 | + sw->text(" [\""); |
167 | + sw->text(pair.first); |
168 | + sw->text("\"] = {\n"); |
169 | + |
170 | + switch (pair.second.type) { |
171 | + case PackInfo::Type::kPacked: |
172 | + sw->text(" type = \"packed\",\n"); |
173 | + sw->text( |
174 | + (boost::format(" texture_atlas = %d,\n") % pair.second.texture_atlas).str()); |
175 | + sw->text((boost::format(" rect = { %d, %d, %d, %d },\n") % pair.second.rect.x % |
176 | + pair.second.rect.y % pair.second.rect.w % pair.second.rect.h).str()); |
177 | + break; |
178 | + |
179 | + case PackInfo::Type::kUnpacked: |
180 | + sw->text(" type = \"unpacked\",\n"); |
181 | + break; |
182 | + } |
183 | + sw->text(" },\n"); |
184 | + } |
185 | + sw->text("}\n"); |
186 | + } |
187 | +} |
188 | + |
189 | +// Pack the images in 'filenames' into texture atlases. |
190 | +std::vector<std::unique_ptr<Texture>> pack_images(const std::vector<std::string>& filenames, |
191 | + const int max_size, |
192 | + std::map<std::string, PackInfo>* pack_info, |
193 | + Texture* first_texture, |
194 | + TextureAtlas::PackedTexture* first_atlas_packed_texture) { |
195 | + std::vector<std::pair<std::string, std::unique_ptr<Texture>>> to_be_packed; |
196 | + for (const auto& filename : filenames) { |
197 | + std::unique_ptr<Texture> image = load_image(filename, g_fs); |
198 | + const auto area = image->width() * image->height(); |
199 | + if (area < kMaxAreaForTextureAtlas) { |
200 | + to_be_packed.push_back(std::make_pair(filename, std::move(image))); |
201 | + } else { |
202 | + pack_info->insert(std::make_pair(filename, PackInfo{ |
203 | + PackInfo::Type::kUnpacked, 0, Rect(), |
204 | + })); |
205 | + } |
206 | + } |
207 | + |
208 | + TextureAtlas atlas; |
209 | + int packed_texture_index = 0; |
210 | + if (first_texture != nullptr) { |
211 | + atlas.add(*first_texture); |
212 | + packed_texture_index = 1; |
213 | + } |
214 | + for (auto& pair : to_be_packed) { |
215 | + atlas.add(*pair.second); |
216 | + } |
217 | + |
218 | + std::vector<std::unique_ptr<Texture>> texture_atlases; |
219 | + std::vector<TextureAtlas::PackedTexture> packed_textures; |
220 | + atlas.pack(max_size, &texture_atlases, &packed_textures); |
221 | + |
222 | + if (first_texture != nullptr) { |
223 | + assert(first_atlas_packed_texture != nullptr); |
224 | + *first_atlas_packed_texture = std::move(packed_textures[0]); |
225 | + } |
226 | + |
227 | + for (size_t i = 0; i < to_be_packed.size(); ++i) { |
228 | + const auto& packed_texture = packed_textures.at(packed_texture_index++); |
229 | + pack_info->insert( |
230 | + std::make_pair(to_be_packed[i].first, PackInfo{PackInfo::Type::kPacked, |
231 | + packed_texture.texture_atlas, |
232 | + packed_texture.texture->blit_data().rect})); |
233 | + } |
234 | + return texture_atlases; |
235 | +} |
236 | + |
237 | } // namespace |
238 | |
239 | int main(int argc, char** argv) { |
240 | - std::string input_directory; |
241 | - if (parse_arguments(argc, argv, &input_directory)) |
242 | + int max_size; |
243 | + if (parse_arguments(argc, argv, &max_size)) |
244 | return 1; |
245 | |
246 | if (SDL_Init(SDL_INIT_VIDEO) < 0) { |
247 | @@ -75,26 +211,63 @@ |
248 | } |
249 | initialize(); |
250 | |
251 | - std::vector<std::unique_ptr<Texture>> images; |
252 | - std::unique_ptr<FileSystem> input_fs(&FileSystem::create(input_directory)); |
253 | - std::vector<std::string> png_filenames; |
254 | - for (const std::string& filename : input_fs->list_directory("")) { |
255 | - if (boost::ends_with(filename, ".png")) { |
256 | - png_filenames.push_back(filename); |
257 | - images.emplace_back(load_image(filename, input_fs.get())); |
258 | - } |
259 | - } |
260 | - |
261 | - TextureAtlas atlas; |
262 | - for (auto& image : images) { |
263 | - atlas.add(*image); |
264 | - } |
265 | - std::vector<std::unique_ptr<Texture>> new_textures; |
266 | - auto packed_texture = atlas.pack(&new_textures); |
267 | + |
268 | + // For performance reasons, we need to have some images in the first texture |
269 | + // atlas, so that OpenGL texture switches do not happen during (for example) |
270 | + // terrain or road rendering. To ensure this, we separate all images into |
271 | + // two disjunct sets. We than pack all images that should go into the first |
272 | + // texture atlas into a texture atlas. Then, we pack all remaining textures |
273 | + // into a texture atlas, but including the first texture atlas as a singular |
274 | + // image (which will probably be the biggest we allow). |
275 | + // |
276 | + // We have to adjust the sub rectangle rendering for the images in the first |
277 | + // texture atlas in 'pack_info' later, before dumping the results. |
278 | + std::vector<std::string> other_images, images_that_must_be_in_first_atlas; |
279 | + std::unordered_set<std::string> all_images; |
280 | + |
281 | + // For terrain textures. |
282 | + find_images("world/terrains", &all_images, &images_that_must_be_in_first_atlas); |
283 | + // For flags and roads. |
284 | + find_images("tribes/images", &all_images, &images_that_must_be_in_first_atlas); |
285 | + // For UI elements mostly, but we get more than we need really. |
286 | + find_images("pics", &all_images, &images_that_must_be_in_first_atlas); |
287 | + |
288 | + // Add all other images, we do not really cares about the order for these. |
289 | + find_images("world", &all_images, &other_images); |
290 | + find_images("tribes", &all_images, &other_images); |
291 | + assert(images_that_must_be_in_first_atlas.size() + other_images.size() == all_images.size()); |
292 | + |
293 | + std::map<std::string, PackInfo> first_texture_atlas_pack_info; |
294 | + auto first_texture_atlas = pack_images(images_that_must_be_in_first_atlas, max_size, |
295 | + &first_texture_atlas_pack_info, nullptr, nullptr); |
296 | + if (first_texture_atlas.size() != 1) { |
297 | + std::cout << "Not all images that should fit in the first texture atlas did actually fit." |
298 | + << std::endl; |
299 | + return 1; |
300 | + } |
301 | + |
302 | + std::map<std::string, PackInfo> pack_info; |
303 | + TextureAtlas::PackedTexture first_atlas_packed_texture; |
304 | + auto texture_atlases = pack_images(other_images, max_size, &pack_info, |
305 | + first_texture_atlas[0].get(), &first_atlas_packed_texture); |
306 | + |
307 | + const auto& blit_data = first_atlas_packed_texture.texture->blit_data(); |
308 | + for (const auto& pair : first_texture_atlas_pack_info) { |
309 | + assert(pack_info.count(pair.first) == 0); |
310 | + pack_info.insert(std::make_pair(pair.first, PackInfo{ |
311 | + pair.second.type, |
312 | + first_atlas_packed_texture.texture_atlas, |
313 | + Rect(blit_data.rect.x + pair.second.rect.x, |
314 | + blit_data.rect.y + pair.second.rect.y, |
315 | + pair.second.rect.w, pair.second.rect.h), |
316 | + })); |
317 | + } |
318 | + |
319 | + // Make sure we have all images. |
320 | + assert(all_images.size() == pack_info.size()); |
321 | |
322 | std::unique_ptr<FileSystem> output_fs(&FileSystem::create(".")); |
323 | - std::unique_ptr<StreamWrite> sw(output_fs->open_stream_write("output.png")); |
324 | - save_to_png(packed_texture.get(), sw.get(), ColorType::RGBA); |
325 | + dump_result(pack_info, &texture_atlases, output_fs.get()); |
326 | |
327 | SDL_Quit(); |
328 | return 0; |
329 | |
330 | === modified file 'src/graphic/texture_atlas.cc' |
331 | --- src/graphic/texture_atlas.cc 2016-01-04 20:54:08 +0000 |
332 | +++ src/graphic/texture_atlas.cc 2016-01-12 21:27:11 +0000 |
333 | @@ -68,22 +68,12 @@ |
334 | return nullptr; |
335 | } |
336 | |
337 | -std::unique_ptr<Texture> TextureAtlas::pack(std::vector<std::unique_ptr<Texture>>* textures) { |
338 | - if (blocks_.empty()) { |
339 | - throw wexception("Called pack() without blocks."); |
340 | - } |
341 | - |
342 | - // Sort blocks by their biggest side length. This heuristically gives the |
343 | - // best packing. |
344 | - std::sort(blocks_.begin(), blocks_.end(), [](const Block& i, const Block& j) { |
345 | - return std::max(i.texture->width(), i.texture->height()) > |
346 | - std::max(j.texture->width(), j.texture->height()); |
347 | - }); |
348 | - |
349 | +std::unique_ptr<Texture> TextureAtlas::pack_as_many_as_possible(const int max_dimension, |
350 | + const int texture_atlas_index, |
351 | + std::vector<PackedTexture>* pack_info) { |
352 | std::unique_ptr<Node> root( |
353 | new Node(Rect(0, 0, blocks_.begin()->texture->width(), blocks_.begin()->texture->height()))); |
354 | |
355 | - // TODO(sirver): when growing, keep maximum size of gl textures in mind. |
356 | const auto grow_right = [&root](int delta_w) { |
357 | std::unique_ptr<Node> new_root(new Node(Rect(0, 0, root->r.w + delta_w, root->r.h))); |
358 | new_root->used = true; |
359 | @@ -100,6 +90,7 @@ |
360 | root.reset(new_root.release()); |
361 | }; |
362 | |
363 | + std::vector<Block> packed, not_packed; |
364 | for (Block& block : blocks_) { |
365 | const int block_width = block.texture->width(); |
366 | const int block_height = block.texture->height(); |
367 | @@ -107,8 +98,10 @@ |
368 | Node* fitting_node = find_node(root.get(), block_width, block_height); |
369 | if (fitting_node == nullptr) { |
370 | // Atlas is not big enough to contain this. Grow it and try again. |
371 | - bool can_grow_down = (block_width <= root->r.w); |
372 | - bool can_grow_right = (block_height <= root->r.h); |
373 | + bool can_grow_down = |
374 | + (block_width <= root->r.w) && (block_height + root->r.h < max_dimension); |
375 | + bool can_grow_right = |
376 | + (block_height <= root->r.h) && (block_width + root->r.w < max_dimension); |
377 | |
378 | // Attempt to keep the texture square-ish. |
379 | bool should_grow_right = can_grow_right && (root->r.h >= root->r.w + block_width); |
380 | @@ -125,34 +118,59 @@ |
381 | } |
382 | fitting_node = find_node(root.get(), block_width, block_height); |
383 | } |
384 | - if (!fitting_node) { |
385 | - throw wexception("Unable to fit node in texture atlas."); |
386 | + if (fitting_node) { |
387 | + fitting_node->split(block_width, block_height); |
388 | + block.node = fitting_node; |
389 | + packed.push_back(block); |
390 | + } else { |
391 | + not_packed.push_back(block); |
392 | } |
393 | - fitting_node->split(block_width, block_height); |
394 | - block.node = fitting_node; |
395 | } |
396 | |
397 | - std::unique_ptr<Texture> packed_texture(new Texture(root->r.w, root->r.h)); |
398 | - packed_texture->fill_rect(Rect(0, 0, root->r.w, root->r.h), RGBAColor(0, 0, 0, 0)); |
399 | - |
400 | - // Sort blocks by index so that they come back in the correct ordering. |
401 | - std::sort(blocks_.begin(), blocks_.end(), [](const Block& i, const Block& j) { |
402 | - return i.index < j.index; |
403 | - }); |
404 | - |
405 | - const auto packed_texture_id = packed_texture->blit_data().texture_id; |
406 | - for (Block& block : blocks_) { |
407 | - packed_texture->blit( |
408 | + std::unique_ptr<Texture> texture_atlas(new Texture(root->r.w, root->r.h)); |
409 | + texture_atlas->fill_rect(Rect(0, 0, root->r.w, root->r.h), RGBAColor(0, 0, 0, 0)); |
410 | + |
411 | + const auto packed_texture_id = texture_atlas->blit_data().texture_id; |
412 | + for (Block& block : packed) { |
413 | + texture_atlas->blit( |
414 | Rect(block.node->r.x, block.node->r.y, block.texture->width(), block.texture->height()), |
415 | *block.texture, |
416 | Rect(0, 0, block.texture->width(), block.texture->height()), |
417 | 1., |
418 | - BlendMode::UseAlpha); |
419 | - |
420 | - textures->emplace_back( |
421 | - new Texture(packed_texture_id, |
422 | - Rect(block.node->r.origin(), block.texture->width(), block.texture->height()), |
423 | - root->r.w, root->r.h)); |
424 | - } |
425 | - return packed_texture; |
426 | + BlendMode::Copy); |
427 | + |
428 | + pack_info->emplace_back(PackedTexture( |
429 | + texture_atlas_index, block.index, |
430 | + std::unique_ptr<Texture>(new Texture( |
431 | + packed_texture_id, |
432 | + Rect(block.node->r.origin(), block.texture->width(), block.texture->height()), |
433 | + root->r.w, root->r.h)))); |
434 | + } |
435 | + blocks_ = not_packed; |
436 | + return texture_atlas; |
437 | +} |
438 | + |
439 | +void TextureAtlas::pack(const int max_dimension, |
440 | + std::vector<std::unique_ptr<Texture>>* texture_atlases, |
441 | + std::vector<PackedTexture>* pack_info) { |
442 | + if (blocks_.empty()) { |
443 | + throw wexception("Called pack() without blocks."); |
444 | + } |
445 | + |
446 | + // Sort blocks by their biggest side length. This heuristically gives the |
447 | + // best packing. |
448 | + std::sort(blocks_.begin(), blocks_.end(), [](const Block& i, const Block& j) { |
449 | + return std::max(i.texture->width(), i.texture->height()) > |
450 | + std::max(j.texture->width(), j.texture->height()); |
451 | + }); |
452 | + |
453 | + while (blocks_.size()) { |
454 | + texture_atlases->emplace_back( |
455 | + pack_as_many_as_possible(max_dimension, texture_atlases->size(), pack_info)); |
456 | + } |
457 | + |
458 | + // Sort pack info by index so that they come back in the correct ordering. |
459 | + std::sort(pack_info->begin(), pack_info->end(), [](const PackedTexture& i, const PackedTexture& j) { |
460 | + return i.index_ < j.index_; |
461 | + }); |
462 | } |
463 | |
464 | === modified file 'src/graphic/texture_atlas.h' |
465 | --- src/graphic/texture_atlas.h 2015-03-01 09:23:10 +0000 |
466 | +++ src/graphic/texture_atlas.h 2016-01-12 21:27:11 +0000 |
467 | @@ -30,6 +30,26 @@ |
468 | // http://codeincomplete.com/posts/2011/5/7/bin_packing/. |
469 | class TextureAtlas { |
470 | public: |
471 | + struct PackedTexture { |
472 | + PackedTexture() : texture_atlas(-1), texture(nullptr), index_(-1) {} |
473 | + |
474 | + // The index of the returned texture atlas that contains this image. |
475 | + int texture_atlas; |
476 | + |
477 | + // The newly packed texture. |
478 | + std::unique_ptr<Texture> texture; |
479 | + |
480 | + private: |
481 | + friend class TextureAtlas; |
482 | + |
483 | + PackedTexture(int init_texture_atlas, int index, std::unique_ptr<Texture> init_texture) |
484 | + : texture_atlas(init_texture_atlas), texture(std::move(init_texture)), index_(index) { |
485 | + } |
486 | + |
487 | + // The position the images was 'add'()ed into the packing queue. Purely internal. |
488 | + int index_; |
489 | + }; |
490 | + |
491 | TextureAtlas(); |
492 | |
493 | // Add 'texture' as one of the textures to be packed. Ownership is |
494 | @@ -37,10 +57,13 @@ |
495 | // called. |
496 | void add(const Image& texture); |
497 | |
498 | - // Packs the textures and returns the packed texture. 'textures' |
499 | - // contains the individual sub textures (that do not own their |
500 | - // memory) in the order they have been added by 'add'. |
501 | - std::unique_ptr<Texture> pack(std::vector<std::unique_ptr<Texture>>* textures); |
502 | + // Packs the textures into as many texture atlases as needed, so that none |
503 | + // of them will be larger than 'max_dimension' x 'max_dimension'. The |
504 | + // returned 'textures' contains the individual sub textures (that do not own |
505 | + // their memory) in the order they have been added by 'add'. |
506 | + void pack(int max_dimension, |
507 | + std::vector<std::unique_ptr<Texture>>* texture_atlases, |
508 | + std::vector<PackedTexture>* textures); |
509 | |
510 | private: |
511 | struct Node { |
512 | @@ -57,14 +80,24 @@ |
513 | |
514 | struct Block { |
515 | Block(int init_index, const Image* init_texture) |
516 | - : index(init_index), texture(init_texture) { |
517 | + : index(init_index), texture(init_texture), done(false) { |
518 | } |
519 | |
520 | + // The index in the order the blocks have been added. |
521 | int index; |
522 | const Image* texture; |
523 | Node* node; |
524 | + |
525 | + // True if this block has already been packed into a texture atlas. |
526 | + bool done; |
527 | }; |
528 | |
529 | + // Packs as many blocks from 'blocks_' that still have done = false into a |
530 | + // fresh texture atlas that will not grow bigger than 'max_size' x |
531 | + // 'max_size'. |
532 | + std::unique_ptr<Texture> pack_as_many_as_possible(const int max_dimension, |
533 | + const int texture_atlas_index, |
534 | + std::vector<PackedTexture>* pack_info); |
535 | static Node* find_node(Node* root, int w, int h); |
536 | |
537 | int next_index_; |
538 | |
539 | === modified file 'src/logic/map_objects/tribes/tribes.cc' |
540 | --- src/logic/map_objects/tribes/tribes.cc 2015-11-28 22:29:26 +0000 |
541 | +++ src/logic/map_objects/tribes/tribes.cc 2016-01-12 21:27:11 +0000 |
542 | @@ -360,17 +360,21 @@ |
543 | } |
544 | } |
545 | |
546 | - std::vector<std::unique_ptr<Texture>> textures; |
547 | - road_texture_ = ta.pack(&textures); |
548 | + std::vector<TextureAtlas::PackedTexture> packed_texture; |
549 | + std::vector<std::unique_ptr<Texture>> texture_atlases; |
550 | + ta.pack(1024, &texture_atlases, &packed_texture); |
551 | + |
552 | + assert(texture_atlases.size() == 1); |
553 | + road_texture_ = std::move(texture_atlases[0]); |
554 | |
555 | size_t next_texture_to_move = 0; |
556 | for (size_t tribeindex = 0; tribeindex < nrtribes(); ++tribeindex) { |
557 | TribeDescr* tribe = tribes_->get_mutable(tribeindex); |
558 | for (size_t i = 0; i < tribe->normal_road_paths().size(); ++i) { |
559 | - tribe->add_normal_road_texture(std::move(textures.at(next_texture_to_move++))); |
560 | + tribe->add_normal_road_texture(std::move(packed_texture.at(next_texture_to_move++).texture)); |
561 | } |
562 | for (size_t i = 0; i < tribe->busy_road_paths().size(); ++i) { |
563 | - tribe->add_busy_road_texture(std::move(textures.at(next_texture_to_move++))); |
564 | + tribe->add_busy_road_texture(std::move(packed_texture.at(next_texture_to_move++).texture)); |
565 | } |
566 | } |
567 | } |
568 | |
569 | === modified file 'src/logic/map_objects/world/world.cc' |
570 | --- src/logic/map_objects/world/world.cc 2016-01-08 21:00:39 +0000 |
571 | +++ src/logic/map_objects/world/world.cc 2016-01-12 21:27:11 +0000 |
572 | @@ -72,14 +72,18 @@ |
573 | } |
574 | } |
575 | |
576 | - std::vector<std::unique_ptr<Texture>> textures; |
577 | - terrain_texture_ = ta.pack(&textures); |
578 | + std::vector<TextureAtlas::PackedTexture> packed_texture; |
579 | + std::vector<std::unique_ptr<Texture>> texture_atlases; |
580 | + ta.pack(1024, &texture_atlases, &packed_texture); |
581 | + |
582 | + assert(texture_atlases.size() == 1); |
583 | + terrain_texture_ = std::move(texture_atlases[0]); |
584 | |
585 | int next_texture_to_move = 0; |
586 | for (size_t i = 0; i < terrains_->size(); ++i) { |
587 | TerrainDescription* terrain = terrains_->get_mutable(i); |
588 | for (size_t j = 0; j < terrain->texture_paths().size(); ++j) { |
589 | - terrain->add_texture(std::move(textures.at(next_texture_to_move++))); |
590 | + terrain->add_texture(std::move(packed_texture.at(next_texture_to_move++).texture)); |
591 | } |
592 | } |
593 | } |
Can the building be part of installation process? On linux, when installing package, usually it is possible to run an "after" script...
On windows - I have no idea how the installation look, but 30 seconds would be bearable...
I dont understand that stuff fully, but an user can pick various resolutions + windowed/fullscreen mode on his/her PC - no problem with this?