From 42d6c1f5f778a54951efea92c177820c2df41b62 Mon Sep 17 00:00:00 2001 From: Steven Wittens Date: Tue, 8 Dec 2009 04:15:10 -0800 Subject: [PATCH] Fix last commit + rewrite face mapper / handedness switch to be clearer --- Resources/Media/OgreCore/New_Ogre_Border.png | Bin 0 -> 924 bytes .../Media/OgreCore/New_Ogre_Border_Break.png | Bin 0 -> 146 bytes .../Media/OgreCore/New_Ogre_Border_Center.png | Bin 0 -> 543 bytes Resources/Media/OgreCore/Ogre.fontdef | 8 + Resources/Media/OgreCore/OgreCore.material | 106 ++ .../Media/OgreCore/OgreDebugPanel.overlay | 154 ++ .../Media/OgreCore/OgreLoadingPanel.overlay | 88 ++ .../Media/OgreCore/OgreProfiler.material | 69 + Resources/Media/OgreCore/ProfileAvgBar.png | Bin 0 -> 938 bytes .../Media/OgreCore/ProfileCurrentBar.png | Bin 0 -> 916 bytes Resources/Media/OgreCore/ProfileMaxBar.png | Bin 0 -> 938 bytes Resources/Media/OgreCore/ProfileMinBar.png | Bin 0 -> 938 bytes Resources/Media/OgreCore/axes.mesh | Bin 0 -> 1894 bytes Resources/Media/OgreCore/axes.png | Bin 0 -> 840 bytes Resources/Media/OgreCore/bluebold.ttf | Bin 0 -> 51728 bytes Resources/Media/OgreCore/bluecond.ttf | Bin 0 -> 52456 bytes Resources/Media/OgreCore/bluehigh.ttf | Bin 0 -> 55820 bytes Resources/Media/OgreCore/ogretext.png | Bin 0 -> 7883 bytes Resources/Media/OgreCore/read_me.html | 2 + .../Media/Planet/Materials/filter.material | 2 +- .../Planet/Materials/normalMapper_FP.glsl | 35 +- .../Planet/Materials/normalMapper_VP.glsl | 3 +- .../Media/Planet/Materials/surface.material | 47 +- Source/Core/Application.cpp | 2 +- Source/Core/ApplicationFrameListener.cpp | 9 +- Source/Core/EngineState.cpp | 8 +- Source/Core/SimpleFrustum.h | 18 + .../PlanetMapBuffer.cpp | 195 +++ Source/DumpBackup/PlanetRenderable kopie.cpp | 1258 +++++++++++++++++ Source/DumpBackup/PlanetRenderable kopie.h | 229 +++ Source/Planet/Map/PlanetEdgeFixup.cpp | 4 +- Source/Planet/Map/PlanetFilter.cpp | 59 +- Source/Planet/Map/PlanetFilter.h | 6 +- Source/Planet/Map/PlanetMap.cpp | 152 +- Source/Planet/Map/PlanetMap.h | 36 +- Source/Planet/Map/PlanetMapBuffer.cpp | 315 ++--- Source/Planet/Map/PlanetMapBuffer.h | 33 +- Source/Planet/Mesh/PlanetCube.cpp | 51 +- Source/Planet/Mesh/PlanetCube.h | 8 +- Source/Planet/Mesh/PlanetCubeTree.cpp | 52 +- Source/Planet/Mesh/PlanetCubeTree.h | 4 + Source/Planet/Mesh/PlanetRenderable.cpp | 310 ++-- Source/Planet/Mesh/PlanetRenderable.h | 8 +- Source/Planet/PlanetMovable.cpp | 39 +- Source/Planet/PlanetMovable.h | 17 +- 45 files changed, 2767 insertions(+), 560 deletions(-) create mode 100644 Resources/Media/OgreCore/New_Ogre_Border.png create mode 100644 Resources/Media/OgreCore/New_Ogre_Border_Break.png create mode 100644 Resources/Media/OgreCore/New_Ogre_Border_Center.png create mode 100644 Resources/Media/OgreCore/Ogre.fontdef create mode 100644 Resources/Media/OgreCore/OgreCore.material create mode 100644 Resources/Media/OgreCore/OgreDebugPanel.overlay create mode 100644 Resources/Media/OgreCore/OgreLoadingPanel.overlay create mode 100644 Resources/Media/OgreCore/OgreProfiler.material create mode 100644 Resources/Media/OgreCore/ProfileAvgBar.png create mode 100644 Resources/Media/OgreCore/ProfileCurrentBar.png create mode 100644 Resources/Media/OgreCore/ProfileMaxBar.png create mode 100644 Resources/Media/OgreCore/ProfileMinBar.png create mode 100644 Resources/Media/OgreCore/axes.mesh create mode 100644 Resources/Media/OgreCore/axes.png create mode 100644 Resources/Media/OgreCore/bluebold.ttf create mode 100644 Resources/Media/OgreCore/bluecond.ttf create mode 100644 Resources/Media/OgreCore/bluehigh.ttf create mode 100644 Resources/Media/OgreCore/ogretext.png create mode 100644 Resources/Media/OgreCore/read_me.html create mode 100644 Source/DumpBackup/PlanetMapBuffer w 6 separate dilated textures/PlanetMapBuffer.cpp create mode 100644 Source/DumpBackup/PlanetRenderable kopie.cpp create mode 100644 Source/DumpBackup/PlanetRenderable kopie.h diff --git a/Resources/Media/OgreCore/New_Ogre_Border.png b/Resources/Media/OgreCore/New_Ogre_Border.png new file mode 100644 index 0000000000000000000000000000000000000000..1a77b519bb486e78f9aaf764621c11aa0bde48fb GIT binary patch literal 924 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6%Z;_2(keu-N~OibYU&Mk+5 zLYxI2k;M!Q+(IDCcj|9R{BdyIcB{@zjdzV@p?f-GZ$!6+IHfe{k|8*?&t;ucLK6UtlIE=d literal 0 HcmV?d00001 diff --git a/Resources/Media/OgreCore/New_Ogre_Border_Break.png b/Resources/Media/OgreCore/New_Ogre_Border_Break.png new file mode 100644 index 0000000000000000000000000000000000000000..1d92686d73ddf6dbaa1d6cc7bddc6ddbda84b88a GIT binary patch literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz_7YEDSN2QXGGb!N5t&!70);pW zJR*x37`TN%nDNrxx<5ccLr)jSkcwMx&p8S*7;vy`4ElfRLkW-8j}+brf cDnbGY(&EQiIMpwIdJhuxboFyt=akR{0JB&n-~a#s literal 0 HcmV?d00001 diff --git a/Resources/Media/OgreCore/New_Ogre_Border_Center.png b/Resources/Media/OgreCore/New_Ogre_Border_Center.png new file mode 100644 index 0000000000000000000000000000000000000000..04335d23d7a7af8d754e4d9e3cd9de63b2b75b5d GIT binary patch literal 543 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58aUX12BR0pvHZkE{-7;x87b`$lJgmz~bmp@TGB)&@XY5CiYbo8;wu? z4xS%-{`C%Vh0NdYX38<1`5}M6&X#G$LIwpNMu!O8$890I&1eT3T c&@o8jY<@bY`*7U50*qb;Pgg&ebxsLQ0L#sO4*&oF literal 0 HcmV?d00001 diff --git a/Resources/Media/OgreCore/Ogre.fontdef b/Resources/Media/OgreCore/Ogre.fontdef new file mode 100644 index 0000000..6fae7e7 --- /dev/null +++ b/Resources/Media/OgreCore/Ogre.fontdef @@ -0,0 +1,8 @@ +BlueHighway +{ + type truetype + source bluehigh.ttf + size 32 + resolution 55 +} + diff --git a/Resources/Media/OgreCore/OgreCore.material b/Resources/Media/OgreCore/OgreCore.material new file mode 100644 index 0000000..9c076eb --- /dev/null +++ b/Resources/Media/OgreCore/OgreCore.material @@ -0,0 +1,106 @@ + +material Core/NodeMaterial +{ + technique + { + pass + { + lighting off + scene_blend add + depth_check off + depth_write off + cull_hardware none + + texture_unit + { + texture axes.png + } + } + } +} +material Core/StatsBlockBorder +{ + technique + { + pass + { + lighting off + scene_blend alpha_blend + depth_check off + + texture_unit + { + texture New_Ogre_Border.png + } + } + } +} +material Core/StatsBlockCenter +{ + technique + { + pass + { + lighting off + scene_blend alpha_blend + depth_check off + + texture_unit + { + texture New_Ogre_Border_Center.png + } + } + } +} + +material Core/ProgressBar +{ + technique + { + pass + { + lighting off + depth_check off + + texture_unit + { + colour_op_ex source1 src_manual src_current 1 1 0.7 + } + } + } +} + +material Core/OgreText +{ + technique + { + pass + { + lighting off + scene_blend alpha_blend + depth_check off + + texture_unit + { + texture ogretext.png + } + } + } +} +material Core/StatsBreak +{ + technique + { + pass + { + lighting off + scene_blend alpha_blend + depth_check off + + texture_unit + { + texture New_Ogre_Border_Break.png + } + } + } +} diff --git a/Resources/Media/OgreCore/OgreDebugPanel.overlay b/Resources/Media/OgreCore/OgreDebugPanel.overlay new file mode 100644 index 0000000..d8ef48a --- /dev/null +++ b/Resources/Media/OgreCore/OgreDebugPanel.overlay @@ -0,0 +1,154 @@ +// Ogre overlay scripts +Core/DebugOverlay +{ + zorder 500 + // Stats block + container BorderPanel(Core/StatPanel) + { + metrics_mode pixels + vert_align bottom + left 5 + top -107 + width 220 + height 102 + material Core/StatsBlockCenter + border_size 1 1 1 1 + border_material Core/StatsBlockBorder + border_topleft_uv 0.0000 1.0000 0.0039 0.9961 + border_top_uv 0.0039 1.0000 0.9961 0.9961 + border_topright_uv 0.9961 1.0000 1.0000 0.9961 + border_left_uv 0.0000 0.9961 0.0039 0.0039 + border_right_uv 0.9961 0.9961 1.0000 0.0039 + border_bottomleft_uv 0.0000 0.0039 0.0039 0.0000 + border_bottom_uv 0.0039 0.0039 0.9961 0.0000 + border_bottomright_uv 0.9961 0.0039 1.0000 0.0000 + + container Panel(Core/BreakPanel) + { + metrics_mode pixels + left 5 + top 22 + width 210 + height 1 + material Core/StatsBreak + } + + element TextArea(Core/CurrFps) + { + metrics_mode pixels + left 5 + top 5 + width 90 + height 30 + font_name BlueHighway + char_height 19 + caption Current FPS: + colour_top 1 1 0.7 + colour_bottom 1 1 0.7 + } + element TextArea(Core/AverageFps) + { + metrics_mode pixels + left 5 + top 25 + width 90 + height 30 + font_name BlueHighway + char_height 16 + caption AVERAGE FPS: + colour_top 0.5 0.7 0.5 + colour_bottom 0.3 0.5 0.3 + } + element TextArea(Core/WorstFps) + { + metrics_mode pixels + left 5 + top 40 + width 90 + height 30 + font_name BlueHighway + char_height 16 + caption WORST FPS: + colour_top 0.5 0.7 0.5 + colour_bottom 0.3 0.5 0.3 + } + element TextArea(Core/BestFps) + { + metrics_mode pixels + left 5 + top 55 + width 90 + height 30 + font_name BlueHighway + char_height 16 + caption BEST FPS: + colour_top 0.5 0.7 0.5 + colour_bottom 0.3 0.5 0.3 + } + element TextArea(Core/NumTris) + { + metrics_mode pixels + left 5 + top 70 + width 90 + height 30 + font_name BlueHighway + char_height 16 + caption Triangle Count: + colour_top 0.5 0.7 0.5 + colour_bottom 0.3 0.5 0.3 + } + element TextArea(Core/NumBatches) + { + metrics_mode pixels + left 5 + top 85 + width 90 + height 30 + font_name BlueHighway + char_height 16 + caption Batch Count: + colour_top 0.5 0.7 0.5 + colour_bottom 0.3 0.5 0.3 + } + element TextArea(Core/DebugText) + { + metrics_mode pixels + left 230 + top 70 + width 200 + height 30 + font_name BlueHighway + char_height 16 + colour_top 0.5 0.7 0.5 + colour_bottom 0.3 0.5 0.3 + } + + } + container Panel(Core/LogoPanel) + { + metrics_mode pixels + horz_align right + vert_align bottom + top -75 + left -165 + width 150 + height 75 + material Core/OgreText + } +} + +// A silly example of how you would do a 3D cockpit +//Examples/KnotCockpit +//{ +// zorder 100 +// entity knot.mesh(hudKnot) +// { +// position 0 0 -50 +// rotation 0 0 0 0 +// } +// +//} + + + diff --git a/Resources/Media/OgreCore/OgreLoadingPanel.overlay b/Resources/Media/OgreCore/OgreLoadingPanel.overlay new file mode 100644 index 0000000..7fb52ae --- /dev/null +++ b/Resources/Media/OgreCore/OgreLoadingPanel.overlay @@ -0,0 +1,88 @@ +// Ogre overlay scripts +Core/LoadOverlay +{ + zorder 510 + // Main panel block + container BorderPanel(Core/LoadPanel) + { + metrics_mode pixels + vert_align center + horz_align center + left -200 + top -45 + width 400 + height 80 + material Core/StatsBlockCenter + border_size 1 1 1 1 + border_material Core/StatsBlockBorder + border_topleft_uv 0.0000 1.0000 0.0039 0.9961 + border_top_uv 0.0039 1.0000 0.9961 0.9961 + border_topright_uv 0.9961 1.0000 1.0000 0.9961 + border_left_uv 0.0000 0.9961 0.0039 0.0039 + border_right_uv 0.9961 0.9961 1.0000 0.0039 + border_bottomleft_uv 0.0000 0.0039 0.0039 0.0000 + border_bottom_uv 0.0039 0.0039 0.9961 0.0000 + border_bottomright_uv 0.9961 0.0039 1.0000 0.0000 + + element TextArea(Core/LoadPanel/Description) + { + metrics_mode pixels + left 5 + top 5 + width 300 + height 20 + font_name BlueHighway + char_height 19 + caption Loading, please wait... + colour_top 1 1 0.7 + colour_bottom 1 1 0.7 + } + + container BorderPanel(Core/LoadPanel/Bar) + { + metrics_mode pixels + vert_align top + left 10 + top 25 + width 380 + height 30 + material Core/StatsBlockCenter + border_size 1 1 1 1 + border_material Core/StatsBlockBorder + border_topleft_uv 0.0000 1.0000 0.0039 0.9961 + border_top_uv 0.0039 1.0000 0.9961 0.9961 + border_topright_uv 0.9961 1.0000 1.0000 0.9961 + border_left_uv 0.0000 0.9961 0.0039 0.0039 + border_right_uv 0.9961 0.9961 1.0000 0.0039 + border_bottomleft_uv 0.0000 0.0039 0.0039 0.0000 + border_bottom_uv 0.0039 0.0039 0.9961 0.0000 + border_bottomright_uv 0.9961 0.0039 1.0000 0.0000 + + element Panel(Core/LoadPanel/Bar/Progress) + { + metrics_mode pixels + left 0 + top 2 + width 30 + height 26 + material Core/ProgressBar + } + } + element TextArea(Core/LoadPanel/Comment) + { + metrics_mode pixels + left 5 + top 60 + width 300 + height 20 + font_name BlueHighway + char_height 15 + caption Initialising... + colour_top 0.8 0.8 0.5 + colour_bottom 0.8 0.8 0.5 + } + + } +} + + diff --git a/Resources/Media/OgreCore/OgreProfiler.material b/Resources/Media/OgreCore/OgreProfiler.material new file mode 100644 index 0000000..a3592bf --- /dev/null +++ b/Resources/Media/OgreCore/OgreProfiler.material @@ -0,0 +1,69 @@ + +material Core/ProfilerCurrent +{ + technique + { + pass + { + lighting off + scene_blend alpha_blend + depth_check off + + texture_unit + { + texture ProfileCurrentBar.png + } + } + } +} +material Core/ProfilerAvg +{ + technique + { + pass + { + lighting off + scene_blend alpha_blend + depth_check off + + texture_unit + { + texture ProfileAvgBar.png + } + } + } +} +material Core/ProfilerMin +{ + technique + { + pass + { + lighting off + scene_blend alpha_blend + depth_check off + + texture_unit + { + texture ProfileMinBar.png + } + } + } +} +material Core/ProfilerMax +{ + technique + { + pass + { + lighting off + scene_blend alpha_blend + depth_check off + + texture_unit + { + texture ProfileMaxBar.png + } + } + } +} \ No newline at end of file diff --git a/Resources/Media/OgreCore/ProfileAvgBar.png b/Resources/Media/OgreCore/ProfileAvgBar.png new file mode 100644 index 0000000000000000000000000000000000000000..2ea025d19e3c7ad03b956a220b9c44ef9f933876 GIT binary patch literal 938 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6&6|H(?D8gCb z5n0T@z%2;EjP)t;6M=&4C7!;n?3X#2`E?nkSE>ABU|=@)ba4!+nDh3Uqac$b1M@+d z(zp-r*{jQHYgQu&X%Q~loCIGZUs1*PJ literal 0 HcmV?d00001 diff --git a/Resources/Media/OgreCore/ProfileCurrentBar.png b/Resources/Media/OgreCore/ProfileCurrentBar.png new file mode 100644 index 0000000000000000000000000000000000000000..c360dd4493fb080a7b4e60e7ac4354a5e57823bc GIT binary patch literal 916 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6&6|H(?D8gCb z5n0T@z%2yAjF;}#{Q(NHmw5WRvR~$86%yCA_4_%Wfq_}k)5S5QV$Rzuj)Dgi7!GVO ze7xg;F1t~MN#KR><2SSG94`I0VyHOIpm1gsjfTL;3xPLG|M#*A?y6Sr1LhwFPgg&e IbxsLQ0Hlr>F#rGn literal 0 HcmV?d00001 diff --git a/Resources/Media/OgreCore/ProfileMaxBar.png b/Resources/Media/OgreCore/ProfileMaxBar.png new file mode 100644 index 0000000000000000000000000000000000000000..0532d950b95ee0edc40f892fd16317d06cbd1947 GIT binary patch literal 938 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6&6|H(?D8gCb z5n0T@z%2;EjP)t;6M=&4C7!;n?3X#2`E{6P?{EQ{!))&9;uumf=j}DeLS{z>X2;l5 zF=yuc_N1LX^m|4gOO@|S%jbXiJ*skV&wE~5$^OCndCj)B@4xXqkYj8x7)7HYFk(Vr o!}V=%=bV2kdw^Arp{16Q^&0mE#|H(?D8gCb z5n0T@z%2;EjP)t;6M=&4C7!;n?3X#2`E^w7Hhs)uU|=@)ba4!+nDh3Uqac$b1M@+d z(zp-r*{Q!DkeWhQNpk ofj9TIZT?(a#qptDj-ll*1N&9(4N2!6KzWeC)78&qol`;+0HB?xnE(I) literal 0 HcmV?d00001 diff --git a/Resources/Media/OgreCore/axes.mesh b/Resources/Media/OgreCore/axes.mesh new file mode 100644 index 0000000000000000000000000000000000000000..04722c5d4ab9ac9d1f5baa29043f93dc3d4e5723 GIT binary patch literal 1894 zcmZuyeN0iR%a|;VDW*DQ5 ziHRCAF_G|>kt`9Ti5q5hkS*akWBi&QbFwX)B!W(4ZV}>KqGsgzyZ61Wh3uWY^UnSK z&euEl-IkQyjnQYjTcUkEodqC)6Gaznd;IjIPk3%U?e1k4TXBo7sn7g7RCrBd=i%3yhbltN6)EAdibUig}c(1tRlHPAtk^tKdVszOeEOMC3`X{8pZte;Ggjm7DWz z#a2!EerC88-|8vk)2B)Vug@6Ygbe;N?}dXi7}KfO?^#VcihWEe)kFgP@Z{Sz79z#e8T zo!c6C^IjB@R$cz-n{_(Cz`6ng$yze>Z;YavM+gZ%*{5pOxQWvc9A12_CrcccMS-d%>slA9l6z z{@6|M!O#z97HfXK>N+LP9?l@!1CxPES6aC1)-N)C>2wbtlY5Zd|3gDh@nrdR$!Fq> z4z1u{{G^$w_n$<({*(L$QsNuidH=!NiFp2Q1^-RQJ|1bClRVMqH5bcfKTbVA*S_4% tO;;B*B`E$IF`+>po|hx9=vQ!`cq^jcP_~7{J#F6>8d!WUL?apS_#c@Fp3(pS literal 0 HcmV?d00001 diff --git a/Resources/Media/OgreCore/axes.png b/Resources/Media/OgreCore/axes.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b2ad11a7c82e5c36479bed47e9445c299407b GIT binary patch literal 840 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 literal 0 HcmV?d00001 diff --git a/Resources/Media/OgreCore/bluecond.ttf b/Resources/Media/OgreCore/bluecond.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4c5b01f95e23e43d66e81a04625421ab5575e4f2 GIT binary patch literal 52456 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 literal 0 HcmV?d00001 diff --git a/Resources/Media/OgreCore/bluehigh.ttf b/Resources/Media/OgreCore/bluehigh.ttf new file mode 100644 index 0000000000000000000000000000000000000000..14d45c5a1eba1193fca02fa724856c0dd6559ceb GIT binary patch literal 55820 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 literal 0 HcmV?d00001 diff --git a/Resources/Media/OgreCore/ogretext.png b/Resources/Media/OgreCore/ogretext.png new file mode 100644 index 0000000000000000000000000000000000000000..8a105928859140cfd66058b5b07e8d1783c1a0ba GIT binary patch literal 7883 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 A#Q*>R literal 0 HcmV?d00001 diff --git a/Resources/Media/OgreCore/read_me.html b/Resources/Media/OgreCore/read_me.html new file mode 100644 index 0000000..ec35f17 --- /dev/null +++ b/Resources/Media/OgreCore/read_me.html @@ -0,0 +1,2 @@ +Larabie Fonts "read me" file, license and FAQ

LARABIE FONTS “README.TXT”

All Larabie Fonts in this file are free to use for personal and/or commercial purposes. No payment is necessary to use these fonts for personal or commercial use. For Software Products who want to include Larabie Fonts see the License Agreement below. You can add this font to a website but do not combine fonts into a single archive or alter them in any way.

All Larabie Fonts are free for commercial use but a sample of your product would be gratefully appreciated so I can see how the font looks in use. Contact www.larabiefonts.com/donation.html for mailing information.

Some Larabie Fonts have enhanced and expanded families available for sale at www.typodermic.com.

If you'd like to make a voluntary donation to Larabie Fonts for the use of the free fonts in any amount please go to www.larabiefonts.com/donation.html

I accept CDs, magazines, t-shirts, a sample of your merchandise or anything featuring Larabie Fonts. Please remember to list your item as a ‘gift’ on the customs form or I will have to pay import duties and taxes on the item. Mailing information is provided at the link above.

Font installation help is available at www.larabiefonts.com/help.html

LARABIE FONTS FREQUENTLY ASKED QUESTIONS

  • Q: How do use these fonts in my favourite software?
  • A: In Windows, you take the fonts out of the ZIP archive and place them in your fonts folder which can be found in your Control Panel. The next time you run your software, the font will be available. For example: If you install a new font, the next time you run Microsoft Word, that font will be available in the menu under Format / Font. For anything more complicated, or Mac installation, visit www.larabiefonts.com/help.html
  • Q: How can I use this font in AOL Instant Messenger, MSN Messenger, Outlook, Outlook Express, Euodora or any other email software?
  • A: At the time of this writing (Feb 2004) you can’t. After installing one of my fonts, you may be able to select it in the above applications but the person at the other end won’t see that same thing unless they have the font installed. If you really want to use my fonts in these applications, make sure the people at the other end have the same fonts installed.
  • Q: How can I use these fonts on a web page?
  • A: If you’re creating a web page using Flash, it’s easy. Consult your Flash manual. If you’re using Acrobat, make sure the font embedding settings are turned on. Consult your Acrobat manual. For anything else there are limitations: If you want to use one of my fonts as your main, text font you’re pretty much out of luck unless you explore a font embedding tool such as WEFT but I don’t recommend it. To use my fonts as headings or titles, use image creation software such as The Gimp, Photoshop, Paint Shop Pro, Pixia etc. Save the images as GIF files and place them on your web page. There’s a lot more to it than can be explained here but there are countless books available on web page design.
  • Q: How can I make these fonts bigger?
  • A: All my fonts are infinitely scalable; the limitations are in your software. A common problem is scaling fonts in Microsoft Word. If you choose Format / Font you can type in any number you like under “size”.
  • Q: Are these fonts really free?
  • A: Yes they are. Some fonts such as Neuropol have expanded font families available for sale at www.typodermic.com but the version you downloaded at Larabie Fonts is free.
  • Q: Your licence agreement states that the fonts can’t be altered. Does that mean I can’t mess around with your fonts in Photoshop/Illustrator/Publisher etc?
  • A: Those license restrictions refer to altering the actual fonts themselves, not what you make with them. As long as you don’t alter the font files in font creation software such as FontLab or Fontographer you’re free to create anything you like with them.
  • Q: Can I use your fonts in a logo?
  • A: Yes. But check with a lawyer if you’re not sure. It’s okay with me if you use it but do so at your own risk.
  • Q: Can you make a custom font for me?
  • A: Possibly. Check typodermic.com/custom.html for details. Keep in mind that making fonts is my full-time job so no freebies.
  • Q: I want to sell rubber stamp alphabets, alphabet punches or stencil alphabets using your font designs.
  • A: Contact me first at www.larabiefonts.com/email.html.
  • Q: My software won’t let me embed one of your fonts.
  • A: You may have an old version of one of my fonts. Uninstall it and install a current version on Larabie Fonts.
  • Q: Can you help me find a font?
  • A: I really don’t have the time but if you send a donation, I can give it a try. If not. post your question on my font forum: www.larabiefonts.com/info.html.

LARABIE FONTS END-USER LICENSE AGREEMENT FOR SOFTWARE PRODUCTS

SOFTWARE PRODUCT LICENSE

The SOFTWARE PRODUCT is protected by copyright laws and International copyright treaties, as well as other intellectual property laws and treaties. The SOFTWARE PRODUCT is licensed, not sold.

1. GRANT OF LICENSE. This document grants you the following rights:

- Installation and Use. You may install and use an unlimited number of copies of the SOFTWARE PRODUCT. You may copy and distribute unlimited copies of the SOFTWARE PRODUCT as you receive them, in any medium, provided that you publish on each copy an appropriate copyright notice. Keep intact all the notices that refer to this License and give any other recipients of the fonts a copy of this License along with the fonts.

2. DESCRIPTION OF OTHER RIGHTS AND LIMITATIONS.

- You may modify your copy or copies of the SOFTWARE PRODUCT or any portion of it, provided that you also meet all of these rules:

a) Do not alter in any way alphanumeric characters (A-Z, a-z, 1-9) contained in the font. An exception is converting between formats, here is allowed the nominal distortion that occurs during conversion from second order to third order quadratic curves (TrueType to Postscript) and vice versa.

b) Extra characters may be added; here it is allowed to use curves (shapes) from alphanumeric characters in fonts under same license.

c) It is allowed to modify and remove analpahbetics (punctuation, special characters, ligatures and symbols).

d) The original font name must be retained but can be augmented. (ie. a Font named Blue Highway can be renamed Blue Highway Cyrillic or Blue Highway ANSI, etc.)

e) Character mapping may be altered.

f) If the kerning information is altered or discarded it must be stated in the user notes or documentation.

g) All modifications must be released under this license.

LIMITED WARRANTY NO WARRANTIES. Larabie Fonts expressly disclaims any warranty for the SOFTWARE PRODUCT. The SOFTWARE PRODUCT and any related documentation is provided "as is" without warranty of any kind, either express or implied, including, without limitation, the implied warranties or merchantability, fitness for a particular purpose, or non-infringement. The entire risk arising out of use or performance of the SOFTWARE PRODUCT remains with you.

NO LIABILITY FOR CONSEQUENTIAL DAMAGES. In no event shall Larabie Fonts be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or any other pecuniary loss) arising out of the use of or inability to use this product, even if Larabie Fonts has been advised of the possibility of such damages.

3. MISCELLANEOUS

Should you have any questions concerning this document, or if you desire to contact Larabie Fonts for any reason, please email www.larabiefonts.com/email.html.

+ diff --git a/Resources/Media/Planet/Materials/filter.material b/Resources/Media/Planet/Materials/filter.material index 36ecb84..3f8a668 100644 --- a/Resources/Media/Planet/Materials/filter.material +++ b/Resources/Media/Planet/Materials/filter.material @@ -36,7 +36,7 @@ material Planet/NormalMapper fragment_program_ref normalMapper_FP { - param_named heightField int 0 + param_named heightMap int 0 param_named_auto sampleDistance custom 1 param_named_auto inverseSampleDistance custom 2 param_named_auto heightScale custom 3 diff --git a/Resources/Media/Planet/Materials/normalMapper_FP.glsl b/Resources/Media/Planet/Materials/normalMapper_FP.glsl index 80d8e56..25eeefe 100644 --- a/Resources/Media/Planet/Materials/normalMapper_FP.glsl +++ b/Resources/Media/Planet/Materials/normalMapper_FP.glsl @@ -1,4 +1,4 @@ -uniform samplerCube heightField; +uniform sampler2D heightMap; uniform float sampleDistance; uniform float inverseSampleDistance; uniform float heightScale; @@ -9,29 +9,28 @@ uniform vec3 faceTransform3; void main() { - // 3d world space coordinates of surface - vec3 uvw0 = gl_TexCoord[0].xyz; // 2d uv coordinates relative to viewport/filter surface - vec2 uv1 = gl_TexCoord[1].xy; + vec2 uv1 = gl_TexCoord[0].xy; + vec2 st1 = gl_TexCoord[1].xy; // Recompose faceTransform matrix mat3 faceTransform = mat3(faceTransform1, faceTransform2, faceTransform3); // Prepare pixel jitter offsets in uvw space. - vec3 xOffset = sampleDistance * faceTransform * vec3(1.0, 0.0, 0.0); - vec3 yOffset = sampleDistance * faceTransform * vec3(0.0, 1.0, 0.0); + vec2 xOffset = sampleDistance * vec2(1.0, 0.0); + vec2 yOffset = sampleDistance * vec2(0.0, 1.0); // Calculate horizontal and vertical central differences. - float xDifference = textureCube(heightField, uvw0 + xOffset).r - - textureCube(heightField, uvw0 - xOffset).r; - float yDifference = textureCube(heightField, uvw0 + yOffset).r - - textureCube(heightField, uvw0 - yOffset).r; + float xDifference = texture2D(heightMap, uv1 + xOffset).r + - texture2D(heightMap, uv1 - xOffset).r; + float yDifference = texture2D(heightMap, uv1 + yOffset).r + - texture2D(heightMap, uv1 - yOffset).r; // Prepare (s,t,1) coordinate system (tangential flat plane to warp into sphere). - float s = uv1.x; - float t = uv1.y; + float s = st1.x; + float t = st1.y; float iw = inversesqrt(s * s + t * t + 1.0); - float h = 1.0 + heightScale * textureCube(heightField, uvw0).r; + float h = 1.0 + heightScale * texture2D(heightMap, uv1).r; // Precalculate values. float st = s * t; @@ -51,13 +50,13 @@ void main() { // Calculate final normal vec3 normal = normalize( faceTransform * cross( - jacobian * vec3(2.0, 0, -xDifference * inverseSampleDistance), - jacobian * vec3(0, 2.0, -yDifference * inverseSampleDistance) + jacobian * vec3(2.0, 0, xDifference * inverseSampleDistance), + jacobian * vec3(0, 2.0, yDifference * inverseSampleDistance) ) ); - float col = (normal.r - normal.g) *.707 * .866 + normal.b * .5; +// float col = (normal.r - normal.g) *.707 * .866 + normal.b * .5; -// gl_FragColor = vec4(normal * .5 + vec3(.5, .5, .5), 1.0); - gl_FragColor = vec4(col, col, col, 1.0); + gl_FragColor = vec4(normal * .5 + vec3(.5, .5, .5), 1.0); +// gl_FragColor = vec4(col, col, col, 1.0); } diff --git a/Resources/Media/Planet/Materials/normalMapper_VP.glsl b/Resources/Media/Planet/Materials/normalMapper_VP.glsl index 706e291..d7aea5e 100644 --- a/Resources/Media/Planet/Materials/normalMapper_VP.glsl +++ b/Resources/Media/Planet/Materials/normalMapper_VP.glsl @@ -1,5 +1,4 @@ -void main() -{ +void main() { gl_Position = ftransform(); gl_TexCoord[0] = gl_MultiTexCoord0; gl_TexCoord[1] = gl_MultiTexCoord1; diff --git a/Resources/Media/Planet/Materials/surface.material b/Resources/Media/Planet/Materials/surface.material index f902151..efdb696 100644 --- a/Resources/Media/Planet/Materials/surface.material +++ b/Resources/Media/Planet/Materials/surface.material @@ -1,3 +1,12 @@ +vertex_program planetSurface_VP glsl { + source planetSurface_VP.glsl +} + +fragment_program planetSurface_FP glsl +{ + source planetSurface_FP.glsl +} + // Planet Surface material Planet/Surface { @@ -5,21 +14,45 @@ material Planet/Surface { pass { - ambient 1.0 1.0 1.0 - diffuse 1.0 1.0 1.0 - - cull_hardware none + cull_hardware clockwise lighting off + depth_check on depth_write on scene_blend alpha_blend + + vertex_program_ref planetSurface_VP { + param_named heightMap int 0 + + param_named_auto invScale custom 1 + param_named_auto planePosition custom 2 + param_named_auto planetRadius custom 3 + param_named_auto planetHeight custom 4 + param_named_auto skirtHeight custom 5 + + param_named_auto faceTransform1 custom 6 + param_named_auto faceTransform2 custom 7 + param_named_auto faceTransform3 custom 8 + } + + fragment_program_ref planetSurface_FP { + param_named normalMap int 1 + param_named_auto tint custom 9 + } - texture_unit + texture_unit heightMap + { + tex_address_mode clamp + filtering linear linear none + colour_op replace + } + + texture_unit normalMap { - cubic_texture cube-Buffer1.png combinedUVW tex_address_mode clamp filtering linear linear none colour_op replace } } } -} \ No newline at end of file +} + diff --git a/Source/Core/Application.cpp b/Source/Core/Application.cpp index f0e8185..f5ab073 100644 --- a/Source/Core/Application.cpp +++ b/Source/Core/Application.cpp @@ -180,7 +180,7 @@ void Application::setupViewport() { void Application::setupScene() { //mSceneManager->setDisplaySceneNodes(true); - mVp->setBackgroundColour(Ogre::ColourValue(0.0, 0.0, 0.0)); + mVp->setBackgroundColour(Ogre::ColourValue(0.3, 0.3, 0.3)); // temporary set up mCamera->setPosition(230.0f, 150.0f, 350.0f); diff --git a/Source/Core/ApplicationFrameListener.cpp b/Source/Core/ApplicationFrameListener.cpp index 20733e7..92b0e2f 100644 --- a/Source/Core/ApplicationFrameListener.cpp +++ b/Source/Core/ApplicationFrameListener.cpp @@ -109,7 +109,7 @@ bool ApplicationFrameListener::processUnbufferedKeyInput(const FrameEvent& evt) if(mKeyboard->isKeyDown(OIS::KC_LEFT)) mCamera->yaw(mRotScale); - if( mKeyboard->isKeyDown(OIS::KC_ESCAPE) || mKeyboard->isKeyDown(OIS::KC_Q) ) + if( mKeyboard->isKeyDown(OIS::KC_ESCAPE) ) return false; if( mKeyboard->isKeyDown(OIS::KC_F) && mTimeUntilNextToggle <= 0 ) @@ -168,9 +168,9 @@ bool ApplicationFrameListener::processUnbufferedKeyInput(const FrameEvent& evt) { EngineState::getSingleton().setValue("planet.seed", getInt("planet.seed") + 1); - srand(time(0)); + srand(time(0) % 65536); Real radius = randf(); - Real height = randf() * .2; + Real height = randf() * .1; Real norm = 100.f / (radius + height); EngineState::getSingleton().setValue("planet.radius", radius * norm); EngineState::getSingleton().setValue("planet.height", height * norm); @@ -178,8 +178,7 @@ bool ApplicationFrameListener::processUnbufferedKeyInput(const FrameEvent& evt) PlanetMovable* planetMovable; SceneNode *planetNode = mSceneManager->getSceneNode("PlanetNode"); planetMovable = (PlanetMovable*)planetNode->getAttachedObject(0); - planetMovable->refresh(); - log("Refresh"); + planetMovable->refresh(PlanetMovableFactory::getDefaultDescriptor()); /* mSceneManager->destroyMovableObject((PlanetMovable*)planetNode->getAttachedObject(0)); diff --git a/Source/Core/EngineState.cpp b/Source/Core/EngineState.cpp index a87be36..900e30f 100644 --- a/Source/Core/EngineState.cpp +++ b/Source/Core/EngineState.cpp @@ -35,13 +35,13 @@ EngineState::EngineState() { // Run-time values setValue("planet.lodDetail", 4.f); - setValue("planet.lodLimit", 3); - setValue("planet.radius", 40.0f); - setValue("planet.height", 60.f); + setValue("planet.lodLimit", 1); + setValue("planet.radius", 80.0f); + setValue("planet.height", 20.f); setValue("planet.gridSize", 33); //setValue("planet.seed", 1007); - setValue("planet.seed", 4009); + setValue("planet.seed", 4090); // setValue("planet.seed", (int)(time(0) & 0xFFFF)); setValue("planet.brushes", 350); diff --git a/Source/Core/SimpleFrustum.h b/Source/Core/SimpleFrustum.h index 16156c6..041e698 100644 --- a/Source/Core/SimpleFrustum.h +++ b/Source/Core/SimpleFrustum.h @@ -26,6 +26,24 @@ class SimpleFrustum void setModelViewProjMatrix(Matrix4 m); + inline bool isVisible(const AxisAlignedBox& bound) { + // Get centre of the box + Vector3 centre = bound.getCenter(); + // Get the half-size of the box + Vector3 halfSize = bound.getHalfSize(); + + // For each plane, see if all points are on the negative side + // If so, object is not visible + for (int plane = 0; plane < 6; ++plane) { + Plane::Side side = mPlanes[plane].getSide(centre, halfSize); + if (side == Plane::NEGATIVE_SIDE) { + return false; + } + + } + return true; + } + inline bool isVisible(const Sphere* s) { Vector3 position = s->getCenter(); Real radius = s->getRadius(); diff --git a/Source/DumpBackup/PlanetMapBuffer w 6 separate dilated textures/PlanetMapBuffer.cpp b/Source/DumpBackup/PlanetMapBuffer w 6 separate dilated textures/PlanetMapBuffer.cpp new file mode 100644 index 0000000..7e65b8d --- /dev/null +++ b/Source/DumpBackup/PlanetMapBuffer w 6 separate dilated textures/PlanetMapBuffer.cpp @@ -0,0 +1,195 @@ +/* + * PlanetMapBuffer.cpp + * NFSpace + * + * Created by Steven Wittens on 29/08/09. + * Copyright 2009 __MyCompanyName__. All rights reserved. + * + */ + +#include "PlanetMapBuffer.h" +#include "PlanetCube.h" + +#include "Application.h" +#include "Utility.h" +#include "Planet.h" +#include "PlanetEdgeFixup.h" + +namespace NFSpace { + +const HeightMapPixel PlanetMapBuffer::LEVEL_MIN = 0x0; +const HeightMapPixel PlanetMapBuffer::LEVEL_MID = 0x8000; +const HeightMapPixel PlanetMapBuffer::LEVEL_MAX = 0xFFFF; +const HeightMapPixel PlanetMapBuffer::LEVEL_RANGE = 0xFFFF; + +PlanetMapBuffer::PlanetMapBuffer(SceneManager* sceneManager, Camera* camera, int type, int size, int border, HeightMapPixel fill) +: mSize(size), mBorder(border), mType(type), mSceneManager(sceneManager), mCamera(camera) { + mFill = (float(fill) - LEVEL_MIN) / LEVEL_RANGE; + assert(isPowerOf2(size - 1)); + mFullSize = mSize + 2 * mBorder; + init(); +} + +PlanetMapBuffer::~PlanetMapBuffer() { + for (int i = 0; i < 6; ++i) { + if (mImage[i].getData()) { + OGRE_FREE(mImage[i].getData(), MEMCATEGORY_RENDERSYS); + } + } +} + +void PlanetMapBuffer::init() { + for (int i = 0; i < 6; ++i) { + mTexture[i] = TextureManager::getSingleton().createManual( + getUniqueId("Buffer"), // Name of texture + "PlanetMap", // Name of resource group in which the texture should be created + TEX_TYPE_2D, // Texture type + mFullSize, // Width + mFullSize, // Height + 1, // Depth (Must be 1 for two dimensional textures) + 0, // Number of mipmaps + PF_L16, // Pixel format + TU_RENDERTARGET // usage + ); + + mRenderTexture[i] = mTexture[i]->getBuffer()->getRenderTarget(); + } +} + +void PlanetMapBuffer::renderFace(int face, bool transform, unsigned int clearFrame) { + // Set camera to have a perfect 45deg FOV in the non-border area. + mCamera->setFOVy(Radian(atan(float(mFullSize + 1) / (mSize)) * 2)); + while (mRenderTexture[face]->getNumViewports() > 0) { + mRenderTexture[face]->removeViewport(0); + } + mRenderTexture[face]->addViewport(mCamera); + mRenderTexture[face]->getViewport(0)->setClearEveryFrame((bool)clearFrame, clearFrame); + mRenderTexture[face]->getViewport(0)->setBackgroundColour(ColourValue(mFill, mFill, mFill, 1)); + mRenderTexture[face]->getViewport(0)->setOverlaysEnabled(false); + + // Set camera to cube face transformation. + // Note: cubemap space is left-handed when viewed from inside the cube. + // Flip the basis vector's x coordinates to compensate. + if (transform) { + Matrix3 faceMatrix = PlanetCube::getFaceTransform(face, false); + Quaternion orientation = Quaternion(faceMatrix); + mCamera->setOrientation(orientation); + } + else { + mCamera->setOrientation(Quaternion(1.f, 0.f, 0.f, 0.f)); + } + // Render the frame to the texture. + mRenderTexture[face]->update(); +} + +void PlanetMapBuffer::render(SceneNode* brushes) { + // Add brushes into the scene. + SceneNode* node = mSceneManager->getRootSceneNode()->createChildSceneNode(); + node->addChild(brushes); + + DumpScene(mSceneManager); + + // Render each cube face from the scene graph. + for (int i = 0; i < 6; ++i) { + renderFace(mSceneManager, mCamera, i, true, FBT_COLOUR | FBT_DEPTH); + } + + // Remove brushes. + node->removeChild((short unsigned int)(0)); + delete node; + + edgeFixup(); +} + +void PlanetMapBuffer::edgeFixup() { + // Prepare materials for rendering the cube face edges. + MaterialPtr materialList[6]; + for (int i = 0; i < 6; ++i) { + materialList[i] = MaterialManager::getSingleton().create( + mTexture[i]->getName(), // name + "PlanetMaterial"); + + Pass* pass = materialList[i]->getTechnique(0)->getPass(0); + pass->setCullingMode(CULL_NONE); + pass->setSceneBlending(SBT_REPLACE); + pass->setDepthCheckEnabled(true); + pass->setDepthWriteEnabled(true); + + TextureUnitState *textureUnit = pass->createTextureUnitState(mTexture[i]->getName()); + textureUnit->setTextureAddressingMode(TextureUnitState::TAM_CLAMP); + textureUnit->setTextureFiltering(TFO_NONE); + } + + // Add fix-up node. + SceneNode* node = mSceneManager->getRootSceneNode()->createChildSceneNode(); + + // Fix up each cube face using fixup edges in the scene graph. + for (int i = 0; i < 6; ++i) { + PlanetEdgeFixup* edgeFixup[4]; + for (int j = 0; j < 4; ++j) { + edgeFixup[j] = new PlanetEdgeFixup(i, j, mSize, mBorder, PlanetEdgeFixup::FIX_BORDER, materialList); + node->attachObject(edgeFixup[j]); + } + + // Update face + renderFace(i, false, false); + + // Clean-up + node->detachAllObjects(); + for (int j = 0; j < 4; ++j) { + delete edgeFixup[j]; + } + + } + + // Clean-up + delete node; +} + +void PlanetMapBuffer::save(bool border) { + std::string names[] = { + "right", + "left", + "top", + "bottom", + "front", + "back", + }; + + for (int i = 0; i < 6; ++i) { + // Create system memory buffer to hold pixel data. + PixelFormat pf = PF_L16; + uchar *data = OGRE_ALLOC_T(uchar, mRenderTexture[i]->getWidth() * mRenderTexture[i]->getHeight() * PixelUtil::getNumElemBytes(pf), MEMCATEGORY_RENDERSYS); + PixelBox pb(mRenderTexture[i]->getWidth(), mRenderTexture[i]->getHeight(), 1, pf, data); + + // Load data and create an image object. + mRenderTexture[i]->copyContentsToMemory(pb, RenderTarget::FB_AUTO); + mImage[i] = Image().loadDynamicImage(data, mRenderTexture[i]->getWidth(), mRenderTexture[i]->getHeight(), 1, pf, false, 1, 0); + + // Save as file + mRenderTexture[i]->writeContentsToFile("cube-" + names[i] + ".png"); + + // Crop image if needed. + if (mBorder && !border) { + Image cropped; + cropped = cropImage(mImage[i], mBorder, mBorder, mSize, mSize); + OGRE_FREE(mImage[i].getData(), MEMCATEGORY_RENDERSYS); + mImage[i] = cropped; + } + } +} + +Image* PlanetMapBuffer::getFace(int face) { + return &mImage[face]; +} + +int PlanetMapBuffer::getPixelFormat() { + switch (mType) { + case MAP_TYPE_MONO: + return PF_L16; + case MAP_TYPE_NORMAL: + } +} + +} + diff --git a/Source/DumpBackup/PlanetRenderable kopie.cpp b/Source/DumpBackup/PlanetRenderable kopie.cpp new file mode 100644 index 0000000..f2f5e86 --- /dev/null +++ b/Source/DumpBackup/PlanetRenderable kopie.cpp @@ -0,0 +1,1258 @@ +/* + * PlanetRenderable.cpp + * NFSpace + * + * Created by Steven Wittens on 4/07/09. + * Copyright 2009 __MyCompanyName__. All rights reserved. + * + * Based on TerrainRenderable.cpp (LGPL) + * + */ +#halt + +#include "PlanetRenderable.h" + + + +namespace Ogre +{ + //----------------------------------------------------------------------- +#define MAIN_BINDING 0 +#define DELTA_BINDING 1 + //----------------------------------------------------------------------- + //----------------------------------------------------------------------- + String PlanetRenderable::mType = "PlanetTile"; + //----------------------------------------------------------------------- + //----------------------------------------------------------------------- + + //----------------------------------------------------------------------- + PlanetRenderable::PlanetRenderable(const String& name, OctreeSceneManager* sceneManager) + : Renderable(), MovableObject(name), mSceneManager(sceneManager), mPositionBuffer(0) + { + mForcedRenderLevel = -1; + mLastNextLevel = -1; + + mMinLevelDistSqr = 0; + + mInit = false; + mLightListDirty = true; + MovableObject::mCastShadows = false; + + mOptions = &(mSceneManager->getOptions()); + + + } + //----------------------------------------------------------------------- + PlanetRenderable::~PlanetRenderable() + { + + deleteGeometry(); + } + //----------------------------------------------------------------------- + uint32 PlanetRenderable::getTypeFlags(void) const + { + // return world flag + return SceneManager::WORLD_GEOMETRY_TYPE_MASK; + } + //----------------------------------------------------------------------- + void PlanetRenderable::deleteGeometry() + { + if(mPlanet) + OGRE_DELETE mPlanet; + + if (mPositionBuffer) + OGRE_FREE(mPositionBuffer, MEMCATEGORY_GEOMETRY); + + if (mMinLevelDistSqr != 0) + OGRE_FREE(mMinLevelDistSqr, MEMCATEGORY_GEOMETRY); + } + //----------------------------------------------------------------------- + void PlanetRenderable::initialise(int startx, int startz, + Real* pageHeightData) { + + if (mOptions->maxGeoMipMapLevel != 0) { + int i = (int) 1 << (mOptions->maxGeoMipMapLevel - 1); + + if ((i + 1) > mOptions->tileSize) + { + printf("Invalid maximum mipmap specifed, must be n, such that 2^(n-1)+1 < tileSize \n"); + return ; + } + } + + deleteGeometry(); + + //calculate min and max heights; + Real min = 256000, max = 0; + + mPlanet = OGRE_NEW VertexData; + mPlanet->vertexStart = 0; + mPlanet->vertexCount = mOptions->tileSize * mOptions->tileSize; + + VertexDeclaration* decl = mPlanet->vertexDeclaration; + VertexBufferBinding* bind = mPlanet->vertexBufferBinding; + + // positions + size_t offset = 0; + decl->addElement(MAIN_BINDING, offset, VET_FLOAT3, VES_POSITION); + offset += VertexElement::getTypeSize(VET_FLOAT3); + if (mOptions->lit) + { + decl->addElement(MAIN_BINDING, offset, VET_FLOAT3, VES_NORMAL); + offset += VertexElement::getTypeSize(VET_FLOAT3); + } + // texture coord sets + decl->addElement(MAIN_BINDING, offset, VET_FLOAT2, VES_TEXTURE_COORDINATES, 0); + offset += VertexElement::getTypeSize(VET_FLOAT2); + decl->addElement(MAIN_BINDING, offset, VET_FLOAT2, VES_TEXTURE_COORDINATES, 1); + offset += VertexElement::getTypeSize(VET_FLOAT2); + if (mOptions->coloured) + { + decl->addElement(MAIN_BINDING, offset, VET_COLOUR, VES_DIFFUSE); + offset += VertexElement::getTypeSize(VET_COLOUR); + } + + // Create shared vertex buffer + mMainBuffer = + HardwareBufferManager::getSingleton().createVertexBuffer( + decl->getVertexSize(MAIN_BINDING), + mPlanet->vertexCount, + HardwareBuffer::HBU_STATIC_WRITE_ONLY); + + // Create system memory copy with just positions in it, for use in simple reads + mPositionBuffer = OGRE_ALLOC_T(float, mPlanet->vertexCount * 3, MEMCATEGORY_GEOMETRY); + + bind->setBinding(MAIN_BINDING, mMainBuffer); + + mInit = true; + + mRenderLevel = 1; + + mMinLevelDistSqr = OGRE_ALLOC_T(Real, mOptions->maxGeoMipMapLevel, MEMCATEGORY_GEOMETRY); + + int endx = startx + mOptions->tileSize; + + int endz = startz + mOptions->tileSize; + + Vector3 left, down, here; + + const VertexElement* poselem = decl->findElementBySemantic(VES_POSITION); + const VertexElement* texelem0 = decl->findElementBySemantic(VES_TEXTURE_COORDINATES, 0); + const VertexElement* texelem1 = decl->findElementBySemantic(VES_TEXTURE_COORDINATES, 1); + float* pSysPos = mPositionBuffer; + + unsigned char* pBase = static_cast(mMainBuffer->lock(HardwareBuffer::HBL_DISCARD)); + + for ( int j = startz; j < endz; j++ ) + { + for ( int i = startx; i < endx; i++ ) + { + float *pPos, *pTex0, *pTex1; + poselem->baseVertexPointerToElement(pBase, &pPos); + texelem0->baseVertexPointerToElement(pBase, &pTex0); + texelem1->baseVertexPointerToElement(pBase, &pTex1); + + Real height = pageHeightData[j * mOptions->pageSize + i]; + height = height * mOptions->scale.y; // scale height + + *pSysPos++ = *pPos++ = ( float ) i * mOptions->scale.x; //x + *pSysPos++ = *pPos++ = height; // y + *pSysPos++ = *pPos++ = ( float ) j * mOptions->scale.z; //z + + *pTex0++ = ( float ) i / ( float ) ( mOptions->pageSize - 1 ); + *pTex0++ = ( float ) j / ( float ) ( mOptions->pageSize - 1 ); + + *pTex1++ = ( ( float ) i / ( float ) ( mOptions->tileSize - 1 ) ) * mOptions->detailTile; + *pTex1++ = ( ( float ) j / ( float ) ( mOptions->tileSize - 1 ) ) * mOptions->detailTile; + + if ( height < min ) + min = ( Real ) height; + + if ( height > max ) + max = ( Real ) height; + + pBase += mMainBuffer->getVertexSize(); + } + } + + mMainBuffer->unlock(); + + mBounds.setExtents( + ( Real ) startx * mOptions->scale.x, + min, + ( Real ) startz * mOptions->scale.z, + ( Real ) ( endx - 1 ) * mOptions->scale.x, + max, + ( Real ) ( endz - 1 ) * mOptions->scale.z ); + + + mCenter = Vector3( ( startx * mOptions->scale.x + (endx - 1) * mOptions->scale.x ) / 2, + ( min + max ) / 2, + ( startz * mOptions->scale.z + (endz - 1) * mOptions->scale.z ) / 2 ); + + mBoundingRadius = Math::Sqrt( + Math::Sqr(max - min) + + Math::Sqr((endx - 1 - startx) * mOptions->scale.x) + + Math::Sqr((endz - 1 - startz) * mOptions->scale.z)) / 2; + + Real C = _calculateCFactor(); + + _calculateMinLevelDist2( C ); + + } + //----------------------------------------------------------------------- + void TerrainRenderable::_notifyCurrentCamera( Camera* cam ) + { + MovableObject::_notifyCurrentCamera(cam); + + if ( mForcedRenderLevel >= 0 ) + { + mRenderLevel = mForcedRenderLevel; + return ; + } + + + Vector3 cpos = cam -> getDerivedPosition(); + const AxisAlignedBox& aabb = getWorldBoundingBox(true); + Vector3 diff(0, 0, 0); + diff.makeFloor(cpos - aabb.getMinimum()); + diff.makeCeil(cpos - aabb.getMaximum()); + + Real L = diff.squaredLength(); + + mRenderLevel = -1; + + for (int i = 0; i < mOptions->maxGeoMipMapLevel; i++) { + if (mMinLevelDistSqr[ i ] > L) { + mRenderLevel = i - 1; + break; + } + } + + if (mRenderLevel < 0) + mRenderLevel = mOptions->maxGeoMipMapLevel - 1; + + if (mOptions->lodMorph) { + // Get the next LOD level down + int nextLevel = mNextLevelDown[mRenderLevel]; + if (nextLevel == 0) { + // No next level, so never morph + mLODMorphFactor = 0; + } + else { + // Set the morph such that the morph happens in the last 0.25 of + // the distance range + Real range = mMinLevelDistSqr[nextLevel] - mMinLevelDistSqr[mRenderLevel]; + if (range) { + Real percent = (L - mMinLevelDistSqr[mRenderLevel]) / range; + // scale result so that msLODMorphStart == 0, 1 == 1, clamp to 0 below that + Real rescale = 1.0f / (1.0f - mOptions->lodMorphStart); + mLODMorphFactor = std::max((percent - mOptions->lodMorphStart) * rescale, + static_cast(0.0)); + } + else { + // Identical ranges + mLODMorphFactor = 0.0f; + } + + assert(mLODMorphFactor >= 0 && mLODMorphFactor <= 1); + } + + // Bind the correct delta buffer if it has changed + // nextLevel - 1 since the first entry is for LOD 1 (since LOD 0 never needs it) + if (mLastNextLevel != nextLevel) { + if (nextLevel > 0) { + mTerrain->vertexBufferBinding->setBinding(DELTA_BINDING, + mDeltaBuffers[nextLevel - 1]); + } + else { + // bind dummy (incase bindings checked) + mTerrain->vertexBufferBinding->setBinding(DELTA_BINDING, + mDeltaBuffers[0]); + } + } + mLastNextLevel = nextLevel; + + } + + } + //----------------------------------------------------------------------- + void TerrainRenderable::_updateRenderQueue( RenderQueue* queue ) + { + // Notify need to calculate light list when our sending to render queue + mLightListDirty = true; + + queue->addRenderable(this, mRenderQueueID); + } + //--------------------------------------------------------------------- + void TerrainRenderable::visitRenderables(Renderable::Visitor* visitor, + bool debugRenderables) + { + visitor->visit(this, 0, false); + } + //----------------------------------------------------------------------- + void TerrainRenderable::getRenderOperation( RenderOperation& op ) + { + //setup indexes for vertices and uvs... + + assert( mInit && "Uninitialized" ); + + op.useIndexes = true; + op.operationType = mOptions->useTriStrips ? + RenderOperation::OT_TRIANGLE_STRIP : RenderOperation::OT_TRIANGLE_LIST; + op.vertexData = mTerrain; + op.indexData = getIndexData(); + + + } + //----------------------------------------------------------------------- + void TerrainRenderable::getWorldTransforms( Matrix4* xform ) const + { + *xform = mParentNode->_getFullTransform(); + } + //----------------------------------------------------------------------- + bool TerrainRenderable::_checkSize( int n ) + { + for ( int i = 0; i < 10; i++ ) + { + if ( ( ( 1 << i ) + 1 ) == n ) + return true; + } + + return false; + } + //----------------------------------------------------------------------- + void TerrainRenderable::_calculateMinLevelDist2( Real C ) + { + //level 0 has no delta. + mMinLevelDistSqr[ 0 ] = 0; + + int i, j; + + for ( int level = 1; level < mOptions->maxGeoMipMapLevel; level++ ) + { + mMinLevelDistSqr[ level ] = 0; + + int step = 1 << level; + // The step of the next higher LOD + int higherstep = step >> 1; + + float* pDeltas = 0; + if (mOptions->lodMorph) + { + // Create a set of delta values (store at index - 1 since 0 has none) + mDeltaBuffers[level - 1] = createDeltaBuffer(); + // Lock, but don't discard (we want the pre-initialised zeros) + pDeltas = static_cast( + mDeltaBuffers[level - 1]->lock(HardwareBuffer::HBL_NORMAL)); + } + + for ( j = 0; j < mOptions->tileSize - step; j += step ) + { + for ( i = 0; i < mOptions->tileSize - step; i += step ) + { + /* Form planes relating to the lower detail tris to be produced + For tri lists and even tri strip rows, they are this shape: + x---x + | / | + x---x + For odd tri strip rows, they are this shape: + x---x + | \ | + x---x + */ + + Vector3 v1(_vertex( i, j, 0 ), _vertex( i, j, 1 ), _vertex( i, j, 2 )); + Vector3 v2(_vertex( i + step, j, 0 ), _vertex( i + step, j, 1 ), _vertex( i + step, j, 2 )); + Vector3 v3(_vertex( i, j + step, 0 ), _vertex( i, j + step, 1 ), _vertex( i, j + step, 2 )); + Vector3 v4(_vertex( i + step, j + step, 0 ), _vertex( i + step, j + step, 1 ), _vertex( i + step, j + step, 2 )); + + Plane t1, t2; + bool backwardTri = false; + if (!mOptions->useTriStrips || j % 2 == 0) + { + t1.redefine(v1, v3, v2); + t2.redefine(v2, v3, v4); + } + else + { + t1.redefine(v1, v3, v4); + t2.redefine(v1, v4, v2); + backwardTri = true; + } + + // include the bottommost row of vertices if this is the last row + int zubound = (j == (mOptions->tileSize - step)? step : step - 1); + for ( int z = 0; z <= zubound; z++ ) + { + // include the rightmost col of vertices if this is the last col + int xubound = (i == (mOptions->tileSize - step)? step : step - 1); + for ( int x = 0; x <= xubound; x++ ) + { + int fulldetailx = i + x; + int fulldetailz = j + z; + if ( fulldetailx % step == 0 && + fulldetailz % step == 0 ) + { + // Skip, this one is a vertex at this level + continue; + } + + Real zpct = (Real)z / (Real)step; + Real xpct = (Real)x / (Real)step; + + //interpolated height + Vector3 actualPos( + _vertex( fulldetailx, fulldetailz, 0 ), + _vertex( fulldetailx, fulldetailz, 1 ), + _vertex( fulldetailx, fulldetailz, 2 )); + Real interp_h; + // Determine which tri we're on + if ((xpct + zpct <= 1.0f && !backwardTri) || + (xpct + (1-zpct) <= 1.0f && backwardTri)) + { + // Solve for x/z + interp_h = + (-(t1.normal.x * actualPos.x) + - t1.normal.z * actualPos.z + - t1.d) / t1.normal.y; + } + else + { + // Second tri + interp_h = + (-(t2.normal.x * actualPos.x) + - t2.normal.z * actualPos.z + - t2.d) / t2.normal.y; + } + + Real actual_h = _vertex( fulldetailx, fulldetailz, 1 ); + Real delta = fabs( interp_h - actual_h ); + + Real D2 = delta * delta * C * C; + + if ( mMinLevelDistSqr[ level ] < D2 ) + mMinLevelDistSqr[ level ] = D2; + + // Should be save height difference? + // Don't morph along edges + if (mOptions->lodMorph && + fulldetailx != 0 && fulldetailx != (mOptions->tileSize - 1) && + fulldetailz != 0 && fulldetailz != (mOptions->tileSize - 1) ) + { + // Save height difference + pDeltas[fulldetailx + (fulldetailz * mOptions->tileSize)] = + interp_h - actual_h; + } + + } + + } + } + } + + // Unlock morph deltas if required + if (mOptions->lodMorph) + { + mDeltaBuffers[level - 1]->unlock(); + } + } + + + + // Post validate the whole set + for ( i = 1; i < mOptions->maxGeoMipMapLevel; i++ ) + { + + // Make sure no LOD transition within the tile + // This is especially a problem when using large tiles with flat areas + /* Hmm, this can look bad on some areas, disable for now + Vector3 delta(_vertex(0,0,0), mCenter.y, _vertex(0,0,2)); + delta = delta - mCenter; + Real minDist = delta.squaredLength(); + mMinLevelDistSqr[ i ] = std::max(mMinLevelDistSqr[ i ], minDist); + */ + + //make sure the levels are increasing... + if ( mMinLevelDistSqr[ i ] < mMinLevelDistSqr[ i - 1 ] ) + { + mMinLevelDistSqr[ i ] = mMinLevelDistSqr[ i - 1 ]; + } + } + + // Now reverse traverse the list setting the 'next level down' + Real lastDist = -1; + int lastIndex = 0; + for (i = mOptions->maxGeoMipMapLevel - 1; i >= 0; --i) + { + if (i == mOptions->maxGeoMipMapLevel - 1) + { + // Last one is always 0 + lastIndex = i; + lastDist = mMinLevelDistSqr[i]; + mNextLevelDown[i] = 0; + } + else + { + mNextLevelDown[i] = lastIndex; + if (mMinLevelDistSqr[i] != lastDist) + { + lastIndex = i; + lastDist = mMinLevelDistSqr[i]; + } + } + + } + + + } + //----------------------------------------------------------------------- + void TerrainRenderable::_adjustRenderLevel( int i ) + { + + mRenderLevel = i; + } + //----------------------------------------------------------------------- + Real TerrainRenderable::_calculateCFactor() + { + Real A, T; + + A = 1.0f; + + int vertRes = 0; + if (mOptions->primaryCamera && mOptions->primaryCamera->getViewport()) + { + vertRes = mOptions->primaryCamera->getViewport()->getActualHeight(); + } + else + { + // default to first render target + RenderSystem* rsys = Root::getSingleton().getRenderSystem(); + if (rsys->getRenderTargetIterator().hasMoreElements()) + vertRes = Root::getSingleton().getRenderSystem()->getRenderTargetIterator().getNext()->getHeight(); + else + // oh, just guess + vertRes = 768; + } + + //A = 1 / Math::Tan(Math::AngleUnitsToRadians(opts.primaryCamera->getFOVy())); + // Turn off detail compression at higher FOVs + + + + T = 2 * ( Real ) mOptions->maxPixelError / ( Real ) vertRes; + + return A / T; + } + //----------------------------------------------------------------------- + float TerrainRenderable::getHeightAt( float x, float z ) + { + Vector3 start; + Vector3 end; + + start.x = _vertex( 0, 0, 0 ); + start.y = _vertex( 0, 0, 1 ); + start.z = _vertex( 0, 0, 2 ); + + end.x = _vertex( mOptions->tileSize - 1, mOptions->tileSize - 1, 0 ); + end.y = _vertex( mOptions->tileSize - 1, mOptions->tileSize - 1, 1 ); + end.z = _vertex( mOptions->tileSize - 1, mOptions->tileSize - 1, 2 ); + + /* Safety catch, if the point asked for is outside + * of this tile, it will ask the appropriate tile + */ + + if ( x < start.x ) + { + if ( mNeighbors[ WEST ] != 0 ) + return mNeighbors[ WEST ] ->getHeightAt( x, z ); + else + x = start.x; + } + + if ( x > end.x ) + { + if ( mNeighbors[ EAST ] != 0 ) + return mNeighbors[ EAST ] ->getHeightAt( x, z ); + else + x = end.x; + } + + if ( z < start.z ) + { + if ( mNeighbors[ NORTH ] != 0 ) + return mNeighbors[ NORTH ] ->getHeightAt( x, z ); + else + z = start.z; + } + + if ( z > end.z ) + { + if ( mNeighbors[ SOUTH ] != 0 ) + return mNeighbors[ SOUTH ] ->getHeightAt( x, z ); + else + z = end.z; + } + + + + float x_pct = ( x - start.x ) / ( end.x - start.x ); + float z_pct = ( z - start.z ) / ( end.z - start.z ); + + float x_pt = x_pct * ( float ) ( mOptions->tileSize - 1 ); + float z_pt = z_pct * ( float ) ( mOptions->tileSize - 1 ); + + int x_index = ( int ) x_pt; + int z_index = ( int ) z_pt; + + // If we got to the far right / bottom edge, move one back + if (x_index == mOptions->tileSize - 1) + { + --x_index; + x_pct = 1.0f; + } + else + { + // get remainder + x_pct = x_pt - x_index; + } + if (z_index == mOptions->tileSize - 1) + { + --z_index; + z_pct = 1.0f; + } + else + { + z_pct = z_pt - z_index; + } + + //bilinear interpolate to find the height. + + float t1 = _vertex( x_index, z_index, 1 ); + float t2 = _vertex( x_index + 1, z_index, 1 ); + float b1 = _vertex( x_index, z_index + 1, 1 ); + float b2 = _vertex( x_index + 1, z_index + 1, 1 ); + + float midpoint = (b1 + t2) / 2.0; + + if (x_pct + z_pct <= 1) { + b2 = midpoint + (midpoint - t1); + } else { + t1 = midpoint + (midpoint - b2); + } + + float t = ( t1 * ( 1 - x_pct ) ) + ( t2 * ( x_pct ) ); + float b = ( b1 * ( 1 - x_pct ) ) + ( b2 * ( x_pct ) ); + + float h = ( t * ( 1 - z_pct ) ) + ( b * ( z_pct ) ); + + return h; + } + //----------------------------------------------------------------------- + bool TerrainRenderable::intersectSegment( const Vector3 & start, const Vector3 & end, Vector3 * result ) + { + Vector3 dir = end - start; + Vector3 ray = start; + + //special case... + if ( dir.x == 0 && dir.z == 0 ) + { + if ( ray.y <= getHeightAt( ray.x, ray.z ) ) + { + if ( result != 0 ) + * result = start; + + return true; + } + } + + dir.normalise(); + + //dir.x *= mScale.x; + //dir.y *= mScale.y; + //dir.z *= mScale.z; + + const AxisAlignedBox& box = getBoundingBox(); + //start with the next one... + ray += dir; + + + while ( ! ( ( ray.x < box.getMinimum().x ) || + ( ray.x > box.getMaximum().x ) || + ( ray.z < box.getMinimum().z ) || + ( ray.z > box.getMaximum().z ) ) ) + { + + + float h = getHeightAt( ray.x, ray.z ); + + if ( ray.y <= h ) + { + if ( result != 0 ) + * result = ray; + + return true; + } + + else + { + ray += dir; + } + + } + + if ( ray.x < box.getMinimum().x && mNeighbors[ WEST ] != 0 ) + return mNeighbors[ WEST ] ->intersectSegment( ray, end, result ); + else if ( ray.z < box.getMinimum().z && mNeighbors[ NORTH ] != 0 ) + return mNeighbors[ NORTH ] ->intersectSegment( ray, end, result ); + else if ( ray.x > box.getMaximum().x && mNeighbors[ EAST ] != 0 ) + return mNeighbors[ EAST ] ->intersectSegment( ray, end, result ); + else if ( ray.z > box.getMaximum().z && mNeighbors[ SOUTH ] != 0 ) + return mNeighbors[ SOUTH ] ->intersectSegment( ray, end, result ); + else + { + if ( result != 0 ) + * result = Vector3( -1, -1, -1 ); + + return false; + } + } + //----------------------------------------------------------------------- + Real TerrainRenderable::getSquaredViewDepth(const Camera* cam) const + { + Vector3 diff = mCenter - cam->getDerivedPosition(); + // Use squared length to avoid square root + return diff.squaredLength(); + } + + //----------------------------------------------------------------------- + const LightList& TerrainRenderable::getLights(void) const + { + if (mLightListDirty) + { + getParentSceneNode()->getCreator()->_populateLightList( + mCenter, this->getBoundingRadius(), mLightList); + mLightListDirty = false; + } + return mLightList; + } + //----------------------------------------------------------------------- + IndexData* TerrainRenderable::getIndexData(void) + { + unsigned int stitchFlags = 0; + + if ( mNeighbors[ EAST ] != 0 && mNeighbors[ EAST ] -> mRenderLevel > mRenderLevel ) + { + stitchFlags |= STITCH_EAST; + stitchFlags |= + (mNeighbors[ EAST ] -> mRenderLevel - mRenderLevel) << STITCH_EAST_SHIFT; + } + + if ( mNeighbors[ WEST ] != 0 && mNeighbors[ WEST ] -> mRenderLevel > mRenderLevel ) + { + stitchFlags |= STITCH_WEST; + stitchFlags |= + (mNeighbors[ WEST ] -> mRenderLevel - mRenderLevel) << STITCH_WEST_SHIFT; + } + + if ( mNeighbors[ NORTH ] != 0 && mNeighbors[ NORTH ] -> mRenderLevel > mRenderLevel ) + { + stitchFlags |= STITCH_NORTH; + stitchFlags |= + (mNeighbors[ NORTH ] -> mRenderLevel - mRenderLevel) << STITCH_NORTH_SHIFT; + } + + if ( mNeighbors[ SOUTH ] != 0 && mNeighbors[ SOUTH ] -> mRenderLevel > mRenderLevel ) + { + stitchFlags |= STITCH_SOUTH; + stitchFlags |= + (mNeighbors[ SOUTH ] -> mRenderLevel - mRenderLevel) << STITCH_SOUTH_SHIFT; + } + + // Check preexisting + LevelArray& levelIndex = mSceneManager->_getLevelIndex(); + IndexMap::iterator ii = levelIndex[ mRenderLevel ]->find( stitchFlags ); + IndexData* indexData; + if ( ii == levelIndex[ mRenderLevel ]->end()) + { + // Create + if (mOptions->useTriStrips) + { + indexData = generateTriStripIndexes(stitchFlags); + } + else + { + indexData = generateTriListIndexes(stitchFlags); + } + levelIndex[ mRenderLevel ]->insert( + IndexMap::value_type(stitchFlags, indexData)); + } + else + { + indexData = ii->second; + } + + + return indexData; + + + } + //----------------------------------------------------------------------- + IndexData* TerrainRenderable::generateTriStripIndexes(unsigned int stitchFlags) + { + // The step used for the current level + int step = 1 << mRenderLevel; + // The step used for the lower level + int lowstep = 1 << (mRenderLevel + 1); + + int numIndexes = 0; + + // Calculate the number of indexes required + // This is the number of 'cells' at this detail level x 2 + // plus 3 degenerates to turn corners + int numTrisAcross = (((mOptions->tileSize-1) / step) * 2) + 3; + // Num indexes is number of tris + 2 + int new_length = numTrisAcross * ((mOptions->tileSize-1) / step) + 2; + //this is the maximum for a level. It wastes a little, but shouldn't be a problem. + + IndexData* indexData = OGRE_NEW IndexData; + indexData->indexBuffer = + HardwareBufferManager::getSingleton().createIndexBuffer( + HardwareIndexBuffer::IT_16BIT, + new_length, HardwareBuffer::HBU_STATIC_WRITE_ONLY);//, false); + + mSceneManager->_getIndexCache().mCache.push_back( indexData ); + + unsigned short* pIdx = static_cast( + indexData->indexBuffer->lock(0, + indexData->indexBuffer->getSizeInBytes(), + HardwareBuffer::HBL_DISCARD)); + + // Stripified mesh + for ( int j = 0; j < mOptions->tileSize - 1; j += step ) + { + int i; + // Forward strip + // We just do the |/ here, final | done after + for ( i = 0; i < mOptions->tileSize - 1; i += step ) + { + int x[4], y[4]; + x[0] = x[1] = i; + x[2] = x[3] = i + step; + y[0] = y[2] = j; + y[1] = y[3] = j + step; + + if (j == 0 && (stitchFlags & STITCH_NORTH)) + { + // North reduction means rounding x[0] and x[2] + if (x[0] % lowstep != 0) + { + // Since we know we only drop down one level of LOD, + // removing 1 step of higher LOD should return to lower + x[0] -= step; + } + if (x[2] % lowstep != 0) + { + x[2] -= step; + } + } + + // Never get a south tiling on a forward strip (always finish on + // a backward strip) + + if (i == 0 && (stitchFlags & STITCH_WEST)) + { + // West reduction means rounding y[0] / y[1] + if (y[0] % lowstep != 0) + { + y[0] -= step; + } + if (y[1] % lowstep != 0) + { + y[1] -= step; + } + } + if (i == (mOptions->tileSize - 1 - step) && (stitchFlags & STITCH_EAST)) + { + // East tiling means rounding y[2] & y[3] + if (y[2] % lowstep != 0) + { + y[2] -= step; + } + if (y[3] % lowstep != 0) + { + y[3] -= step; + } + } + + //triangles + if (i == 0) + { + // Starter + *pIdx++ = _index( x[0], y[0] ); numIndexes++; + } + *pIdx++ = _index( x[1], y[1] ); numIndexes++; + *pIdx++ = _index( x[2], y[2] ); numIndexes++; + + if (i == mOptions->tileSize - 1 - step) + { + // Emit extra index to finish row + *pIdx++ = _index( x[3], y[3] ); numIndexes++; + if (j < mOptions->tileSize - 1 - step) + { + // Emit this index twice more (this is to turn around without + // artefacts) + // ** Hmm, looks like we can drop this and it's unnoticeable + //*pIdx++ = _index( x[3], y[3] ); numIndexes++; + //*pIdx++ = _index( x[3], y[3] ); numIndexes++; + } + } + + } + // Increment row + j += step; + // Backward strip + for ( i = mOptions->tileSize - 1; i > 0 ; i -= step ) + { + int x[4], y[4]; + x[0] = x[1] = i; + x[2] = x[3] = i - step; + y[0] = y[2] = j; + y[1] = y[3] = j + step; + + // Never get a north tiling on a backward strip (always + // start on a forward strip) + if (j == (mOptions->tileSize - 1 - step) && (stitchFlags & STITCH_SOUTH)) + { + // South reduction means rounding x[1] / x[3] + if (x[1] % lowstep != 0) + { + x[1] -= step; + } + if (x[3] % lowstep != 0) + { + x[3] -= step; + } + } + + if (i == step && (stitchFlags & STITCH_WEST)) + { + // West tiling on backward strip is rounding of y[2] / y[3] + if (y[2] % lowstep != 0) + { + y[2] -= step; + } + if (y[3] % lowstep != 0) + { + y[3] -= step; + } + } + if (i == mOptions->tileSize - 1 && (stitchFlags & STITCH_EAST)) + { + // East tiling means rounding y[0] and y[1] on backward strip + if (y[0] % lowstep != 0) + { + y[0] -= step; + } + if (y[1] % lowstep != 0) + { + y[1] -= step; + } + } + + //triangles + if (i == mOptions->tileSize) + { + // Starter + *pIdx++ = _index( x[0], y[0] ); numIndexes++; + } + *pIdx++ = _index( x[1], y[1] ); numIndexes++; + *pIdx++ = _index( x[2], y[2] ); numIndexes++; + + if (i == step) + { + // Emit extra index to finish row + *pIdx++ = _index( x[3], y[3] ); numIndexes++; + if (j < mOptions->tileSize - 1 - step) + { + // Emit this index once more (this is to turn around) + *pIdx++ = _index( x[3], y[3] ); numIndexes++; + } + } + } + } + + + indexData->indexBuffer->unlock(); + indexData->indexCount = numIndexes; + indexData->indexStart = 0; + + return indexData; + + } + //----------------------------------------------------------------------- + IndexData* TerrainRenderable::generateTriListIndexes(unsigned int stitchFlags) + { + + int numIndexes = 0; + int step = 1 << mRenderLevel; + + IndexData* indexData = 0; + + int north = stitchFlags & STITCH_NORTH ? step : 0; + int south = stitchFlags & STITCH_SOUTH ? step : 0; + int east = stitchFlags & STITCH_EAST ? step : 0; + int west = stitchFlags & STITCH_WEST ? step : 0; + + int new_length = ( mOptions->tileSize / step ) * ( mOptions->tileSize / step ) * 2 * 2 * 2 ; + //this is the maximum for a level. It wastes a little, but shouldn't be a problem. + + indexData = OGRE_NEW IndexData; + indexData->indexBuffer = + HardwareBufferManager::getSingleton().createIndexBuffer( + HardwareIndexBuffer::IT_16BIT, + new_length, HardwareBuffer::HBU_STATIC_WRITE_ONLY);//, false); + + mSceneManager->_getIndexCache().mCache.push_back( indexData ); + + unsigned short* pIdx = static_cast( + indexData->indexBuffer->lock(0, + indexData->indexBuffer->getSizeInBytes(), + HardwareBuffer::HBL_DISCARD)); + + // Do the core vertices, minus stitches + for ( int j = north; j < mOptions->tileSize - 1 - south; j += step ) + { + for ( int i = west; i < mOptions->tileSize - 1 - east; i += step ) + { + //triangles + *pIdx++ = _index( i, j ); numIndexes++; + *pIdx++ = _index( i, j + step ); numIndexes++; + *pIdx++ = _index( i + step, j ); numIndexes++; + + *pIdx++ = _index( i, j + step ); numIndexes++; + *pIdx++ = _index( i + step, j + step ); numIndexes++; + *pIdx++ = _index( i + step, j ); numIndexes++; + } + } + + // North stitching + if ( north > 0 ) + { + numIndexes += stitchEdge(NORTH, mRenderLevel, mNeighbors[NORTH]->mRenderLevel, + west > 0, east > 0, &pIdx); + } + // East stitching + if ( east > 0 ) + { + numIndexes += stitchEdge(EAST, mRenderLevel, mNeighbors[EAST]->mRenderLevel, + north > 0, south > 0, &pIdx); + } + // South stitching + if ( south > 0 ) + { + numIndexes += stitchEdge(SOUTH, mRenderLevel, mNeighbors[SOUTH]->mRenderLevel, + east > 0, west > 0, &pIdx); + } + // West stitching + if ( west > 0 ) + { + numIndexes += stitchEdge(WEST, mRenderLevel, mNeighbors[WEST]->mRenderLevel, + south > 0, north > 0, &pIdx); + } + + + indexData->indexBuffer->unlock(); + indexData->indexCount = numIndexes; + indexData->indexStart = 0; + + return indexData; + } + //----------------------------------------------------------------------- + HardwareVertexBufferSharedPtr TerrainRenderable::createDeltaBuffer(void) + { + // Delta buffer is a 1D float buffer of height offsets + HardwareVertexBufferSharedPtr buf = + HardwareBufferManager::getSingleton().createVertexBuffer( + VertexElement::getTypeSize(VET_FLOAT1), + mOptions->tileSize * mOptions->tileSize, + HardwareBuffer::HBU_STATIC_WRITE_ONLY); + // Fill the buffer with zeros, we will only fill in delta + void* pVoid = buf->lock(HardwareBuffer::HBL_DISCARD); + memset(pVoid, 0, mOptions->tileSize * mOptions->tileSize * sizeof(float)); + buf->unlock(); + + return buf; + + } + //----------------------------------------------------------------------- + void TerrainRenderable::_updateCustomGpuParameter( + const GpuProgramParameters::AutoConstantEntry& constantEntry, + GpuProgramParameters* params) const + { + if (constantEntry.data == MORPH_CUSTOM_PARAM_ID) + { + // Update morph LOD factor + params->_writeRawConstant(constantEntry.physicalIndex, mLODMorphFactor); + } + else + { + Renderable::_updateCustomGpuParameter(constantEntry, params); + } + + } + //----------------------------------------------------------------------- + int TerrainRenderable::stitchEdge(Neighbor neighbor, int hiLOD, int loLOD, + bool omitFirstTri, bool omitLastTri, unsigned short** ppIdx) + { + assert(loLOD > hiLOD); + /* + Now do the stitching; we can stitch from any level to any level. + The stitch pattern is like this for each pair of vertices in the lower LOD + (excuse the poor ascii art): + + lower LOD + *-----------* + |\ \ 3 / /| + |1\2 \ / 4/5| + *--*--*--*--* + higher LOD + + The algorithm is, for each pair of lower LOD vertices: + 1. Iterate over the higher LOD vertices, generating tris connected to the + first lower LOD vertex, up to and including 1/2 the span of the lower LOD + over the higher LOD (tris 1-2). Skip the first tri if it is on the edge + of the tile and that edge is to be stitched itself. + 2. Generate a single tri for the middle using the 2 lower LOD vertices and + the middle vertex of the higher LOD (tri 3). + 3. Iterate over the higher LOD vertices from 1/2 the span of the lower LOD + to the end, generating tris connected to the second lower LOD vertex + (tris 4-5). Skip the last tri if it is on the edge of a tile and that + edge is to be stitched itself. + + The same algorithm works for all edges of the patch; stitching is done + clockwise so that the origin and steps used change, but the general + approach does not. + */ + + // Get pointer to be updated + unsigned short* pIdx = *ppIdx; + + // Work out the steps ie how to increment indexes + // Step from one vertex to another in the high detail version + int step = 1 << hiLOD; + // Step from one vertex to another in the low detail version + int superstep = 1 << loLOD; + // Step half way between low detail steps + int halfsuperstep = superstep >> 1; + + // Work out the starting points and sign of increments + // We always work the strip clockwise + int startx, starty, endx, rowstep; + bool horizontal; + switch(neighbor) + { + case NORTH: + startx = starty = 0; + endx = mOptions->tileSize - 1; + rowstep = step; + horizontal = true; + break; + case SOUTH: + // invert x AND y direction, helps to keep same winding + startx = starty = mOptions->tileSize - 1; + endx = 0; + rowstep = -step; + step = -step; + superstep = -superstep; + halfsuperstep = -halfsuperstep; + horizontal = true; + break; + case EAST: + startx = 0; + endx = mOptions->tileSize - 1; + starty = mOptions->tileSize - 1; + rowstep = -step; + horizontal = false; + break; + case WEST: + startx = mOptions->tileSize - 1; + endx = 0; + starty = 0; + rowstep = step; + step = -step; + superstep = -superstep; + halfsuperstep = -halfsuperstep; + horizontal = false; + break; + }; + + int numIndexes = 0; + + for ( int j = startx; j != endx; j += superstep ) + { + int k; + for (k = 0; k != halfsuperstep; k += step) + { + int jk = j + k; + //skip the first bit of the corner? + if ( j != startx || k != 0 || !omitFirstTri ) + { + if (horizontal) + { + *pIdx++ = _index( j , starty ); numIndexes++; + *pIdx++ = _index( jk, starty + rowstep ); numIndexes++; + *pIdx++ = _index( jk + step, starty + rowstep ); numIndexes++; + } + else + { + *pIdx++ = _index( starty, j ); numIndexes++; + *pIdx++ = _index( starty + rowstep, jk ); numIndexes++; + *pIdx++ = _index( starty + rowstep, jk + step); numIndexes++; + } + } + } + + // Middle tri + if (horizontal) + { + *pIdx++ = _index( j, starty ); numIndexes++; + *pIdx++ = _index( j + halfsuperstep, starty + rowstep); numIndexes++; + *pIdx++ = _index( j + superstep, starty ); numIndexes++; + } + else + { + *pIdx++ = _index( starty, j ); numIndexes++; + *pIdx++ = _index( starty + rowstep, j + halfsuperstep ); numIndexes++; + *pIdx++ = _index( starty, j + superstep ); numIndexes++; + } + + for (k = halfsuperstep; k != superstep; k += step) + { + int jk = j + k; + if ( j != endx - superstep || k != superstep - step || !omitLastTri ) + { + if (horizontal) + { + *pIdx++ = _index( j + superstep, starty ); numIndexes++; + *pIdx++ = _index( jk, starty + rowstep ); numIndexes++; + *pIdx++ = _index( jk + step, starty + rowstep ); numIndexes++; + } + else + { + *pIdx++ = _index( starty, j + superstep ); numIndexes++; + *pIdx++ = _index( starty + rowstep, jk ); numIndexes++; + *pIdx++ = _index( starty + rowstep, jk + step ); numIndexes++; + } + } + } + } + + *ppIdx = pIdx; + + return numIndexes; + + } + + +} //namespace diff --git a/Source/DumpBackup/PlanetRenderable kopie.h b/Source/DumpBackup/PlanetRenderable kopie.h new file mode 100644 index 0000000..1ad009a --- /dev/null +++ b/Source/DumpBackup/PlanetRenderable kopie.h @@ -0,0 +1,229 @@ +/* + * PlanetRenderable.h + * NFSpace + * + * Created by Steven Wittens on 4/07/09. + * Copyright 2009 __MyCompanyName__. All rights reserved. + * + * Based on TerrainRenderable.h (LGPL) + */ + +#ifndef __PLANET_RENDERABLE_H +#define __PLANET_RENDERABLE_H + +#include "Ogre/OgreSceneNode.h" +#include "Ogre/OgreRenderable.h" +#include "Ogre/OgreRenderQueue.h" +#include "Ogre/OgreRenderOperation.h" +#include "Ogre/OgreCamera.h" +#include "Ogre/OgreRoot.h" +#include "Ogre/OgreLogManager.h" +#include "Ogre/OgreStringConverter.h" +#include "Ogre/OgreViewport.h" +#include "Ogre/OgreException.h" +#include "Ogre/OgreRenderSystem.h" + +using namespace Ogre; + +class PlanetOptions : public GeneralAllocatedObject { +public: + PlanetOptions() + { + pageSize = 0; + tileSize = 0; + tilesPerPage = 0; + maxGeoMipMapLevel = 0; + scale = Vector3::UNIT_SCALE; + maxPixelError = 4; + detailTile = 1; + lit = false; + coloured = false; + lodMorph = false; + lodMorphStart = 0.5; + useTriStrips = false; + primaryCamera = 0; + planetMaterial.setNull(); + }; + /// The size of one edge of a terrain page, in vertices + size_t pageSize; + /// The size of one edge of a terrain tile, in vertices + size_t tileSize; + /// Precalculated number of tiles per page + size_t tilesPerPage; + /// The primary camera, used for error metric calculation and page choice + const Camera* primaryCamera; + /// The maximum terrain geo-mipmap level + size_t maxGeoMipMapLevel; + /// The scale factor to apply to the terrain (each vertex is 1 unscaled unit + /// away from the next, and height is from 0 to 1) + Vector3 scale; + /// The maximum pixel error allowed + size_t maxPixelError; + /// Whether we should use triangle strips + bool useTriStrips; + /// The number of times to repeat a detail texture over a tile + size_t detailTile; + /// Whether LOD morphing is enabled + bool lodMorph; + /// At what point (parametric) should LOD morphing start + Real lodMorphStart; + /// Whether dynamic lighting is enabled + bool lit; + /// Whether vertex colours are enabled + bool coloured; + /// Pointer to the material to use to render the terrain + MaterialPtr planetMaterial; + +}; + +/** + Represents a terrain tile. + @remarks + A TerrainRenderable represents a tile used to render a block of terrain using the geomipmap approach + for LOD. + *@author Jon Anderson + */ + +class PlanetRenderable : public Renderable, public MovableObject +{ +public: + + PlanetRenderable(const String& name, SceneManager* sceneManager); + ~PlanetRenderable(); + + void deleteGeometry(); + + /** Initializes the PlanetRenderable. + @param startx, startz + The starting points of the top-left of this tile, in terms of the + number of vertices. + @param pageHeightData The source height data for the entire parent page + */ + void initialise(int startx, int startz, Real* pageHeightData); + + //movable object methods + + /** Returns the type of the movable. */ + virtual const String& getMovableType(void) const { + return mType; + }; + + /** Returns the bounding box of this PlanetRenderable */ + const AxisAlignedBox& getBoundingBox(void) const { + return mBounds; + }; + + /** Updates the level of detail to be used for rendering this PlanetRenderable based on the passed in Camera */ + virtual void _notifyCurrentCamera(Camera* cam); + + virtual void _updateRenderQueue(RenderQueue* queue); + + /// @copydoc MovableObject::visitRenderables + void visitRenderables(Renderable::Visitor* visitor, + bool debugRenderables = false); + + /** + Constructs a RenderOperation to render the TerrainRenderable. + @remarks + Each TerrainRenderable has a block of vertices that represent the terrain. Index arrays are dynamically + created for mipmap level, and then cached. + */ + virtual void getRenderOperation(RenderOperation& rend); + + virtual const MaterialPtr& getMaterial(void) const { + return mMaterial; + }; + + virtual void getWorldTransforms(Matrix4* xform) const; + + /** Returns the mipmap level that will be rendered for this frame */ + inline int getRenderLevel() const { + return mRenderLevel; + }; + + /** Forces the LOD to the given level from this point on */ + inline void setForcedRenderLevel(int i) { + mForcedRenderLevel = i; + } + + /** Intersects the segment witht he terrain tile + */ + bool intersectSegment(const Vector3& start, const Vector3& end, Vector3* result); + + void setMaterial(const MaterialPtr& m) { + mMaterial = m; + }; + + /** Overridden, see Renderable */ + Real getSquaredViewDepth(const Camera* cam) const; + + /** Overridden from MovableObject */ + Real getBoundingRadius(void) const { return mBoundingRadius; } + + /** @copydoc Renderable::getLights */ + const LightList& getLights(void) const; + + /// Overridden from Renderable to allow the morph LOD entry to be set + void _updateCustomGpuParameter( + const GpuProgramParameters::AutoConstantEntry& constantEntry, + GpuProgramParameters* params) const; + /// @see MovableObject + unsigned int getTypeFlags(void) const; +protected: + /// Parent SceneManager + SceneManager* mSceneManager; + /// Link to shared options + const PlanetOptions* mOptions; + + void _calculateMinLevelDist2(Real C); + + Real _calculateCFactor(); + + VertexData* mTerrain; + + /// The current LOD level + int mRenderLevel; + /// The previous 'next' LOD level down, for frame coherency + int mLastNextLevel; + /// The morph factor between this and the next LOD level down + Real mLODMorphFactor; + /// List of squared distances at which LODs change + Real *mMinLevelDistSqr; + /// Whether light list need to re-calculate + mutable bool mLightListDirty; + /// Cached light list + mutable LightList mLightList; + /// The bounding radius of this tile + Real mBoundingRadius; + /// Bounding box of this tile + AxisAlignedBox mBounds; + /// The center point of this tile + Vector3 mCenter; + /// The MovableObject type + static String mType; + /// Current material used by this tile + MaterialPtr mMaterial; + /// Whether this tile has been initialised + bool mInit; + /// The buffer with all the renderable geometry in it + HardwareVertexBufferSharedPtr mMainBuffer; + /// Optional set of delta buffers, used to morph from one LOD to the next + typedef std::vector VertexBufferList; + VertexBufferList mDeltaBuffers; + /// System-memory buffer with just positions in it, for CPU operations + float* mPositionBuffer; + /// Forced rendering LOD level, optional + int mForcedRenderLevel; + /// Array of LOD indexes specifying which LOD is the next one down + /// (deals with clustered error metrics which cause LODs to be skipped) + int mNextLevelDown[10]; + /// Gets the index data for this tile based on current settings + IndexData* getIndexData(void); + /// Internal method for generating stripified terrain indexes + IndexData* generateTriStripIndexes(unsigned int stitchFlags); + /// Internal method for generating triangle list terrain indexes + IndexData* generateTriListIndexes(unsigned int stitchFlags); + +}; + +#endif diff --git a/Source/Planet/Map/PlanetEdgeFixup.cpp b/Source/Planet/Map/PlanetEdgeFixup.cpp index 97650eb..617524c 100644 --- a/Source/Planet/Map/PlanetEdgeFixup.cpp +++ b/Source/Planet/Map/PlanetEdgeFixup.cpp @@ -114,8 +114,8 @@ namespace NFSpace { } // Find the transform from the current face to the adjacent one. - Matrix3 faceTransform = PlanetCube::getFaceTransform(face, false); - Matrix3 edgeTransform = PlanetCube::getFaceTransform(sEdgeFace[face][edge], false); + Matrix3 faceTransform = PlanetCube::getFaceTransform(face); + Matrix3 edgeTransform = PlanetCube::getFaceTransform(sEdgeFace[face][edge]); edgeTransform = fixAlign * (edgeTransform.Transpose() * faceTransform); if (fix & FIX_SIDES) { diff --git a/Source/Planet/Map/PlanetFilter.cpp b/Source/Planet/Map/PlanetFilter.cpp index ac0dbca..0bfafc6 100644 --- a/Source/Planet/Map/PlanetFilter.cpp +++ b/Source/Planet/Map/PlanetFilter.cpp @@ -16,31 +16,32 @@ using namespace Ogre; namespace NFSpace { - PlanetFilter::PlanetFilter(int face, int size, int border) : SimpleRenderable() { - initRenderOp(face); - initVertexData(face, size, border); + PlanetFilter::PlanetFilter(int face, int lod, int x, int y, int size, int border) : SimpleRenderable() { + initRenderOp(); + initVertexData(lod, x, y, size, border); Real height = getReal("planet.height"); Real radius = getReal("planet.radius"); // SampleDistance - setCustomParameter(1, Vector4(2.0 / size, 0, 0, 0)); + setCustomParameter(1, Vector4(1.0 / (size + border), 0, 0, 0)); // inverseSampleDistance - setCustomParameter(2, Vector4(size * .5, 0, 0, 0)); + setCustomParameter(2, Vector4((border + size) * .5, 0, 0, 0)); // heightScale setCustomParameter(3, Vector4(height / radius, 0, 0, 0)); - Matrix3 faceTransform = PlanetCube::getFaceTransform(face, false); - setCustomParameter(4, Vector4(faceTransform[0][0], faceTransform[0][1], faceTransform[0][2], 0)); - setCustomParameter(5, Vector4(faceTransform[1][0], faceTransform[1][1], faceTransform[1][2], 0)); - setCustomParameter(6, Vector4(faceTransform[2][0], faceTransform[2][1], faceTransform[2][2], 0)); + // Something weird here with vertical axis mapping. + Matrix3 faceTransform = PlanetCube::getFaceTransform(face); + setCustomParameter(4, Vector4(faceTransform[0][0], faceTransform[1][0], faceTransform[2][0], 0)); + setCustomParameter(5, Vector4(faceTransform[0][1], faceTransform[1][1], faceTransform[2][1], 0)); + setCustomParameter(6, Vector4(faceTransform[0][2], faceTransform[1][2], faceTransform[2][2], 0)); } PlanetFilter::~PlanetFilter() { delete mRenderOp.vertexData; } - void PlanetFilter::initRenderOp(int face) { + void PlanetFilter::initRenderOp() { mRenderOp.operationType = RenderOperation::OT_TRIANGLE_STRIP; mRenderOp.useIndexes = FALSE; mRenderOp.vertexData = new VertexData(); @@ -49,7 +50,7 @@ namespace NFSpace { } - void PlanetFilter::initVertexData(int face, int size, int border) { + void PlanetFilter::initVertexData(int lod, int x, int y, int size, int border) { // Prepare new vertex buffer VertexData* vertexData = mRenderOp.vertexData; @@ -57,8 +58,8 @@ namespace NFSpace { size_t offset = 0; vertexDeclaration->addElement(0, offset, VET_FLOAT3, VES_POSITION); offset += VertexElement::getTypeSize(VET_FLOAT3); - vertexDeclaration->addElement(0, offset, VET_FLOAT3, VES_TEXTURE_COORDINATES, 0); - offset += VertexElement::getTypeSize(VET_FLOAT3); + vertexDeclaration->addElement(0, offset, VET_FLOAT2, VES_TEXTURE_COORDINATES, 0); + offset += VertexElement::getTypeSize(VET_FLOAT2); vertexDeclaration->addElement(0, offset, VET_FLOAT2, VES_TEXTURE_COORDINATES, 1); offset += VertexElement::getTypeSize(VET_FLOAT2); @@ -68,19 +69,21 @@ namespace NFSpace { vertexData->vertexBufferBinding->setBinding(0, vertexBuffer); vertexData->vertexCount = 0; - // Get offsets / pointers + // Get offsets / pointers into vertex declaration const VertexElement* poselem = vertexDeclaration->findElementBySemantic(VES_POSITION); const VertexElement* texelem1 = vertexDeclaration->findElementBySemantic(VES_TEXTURE_COORDINATES, 0); const VertexElement* texelem2 = vertexDeclaration->findElementBySemantic(VES_TEXTURE_COORDINATES, 1); unsigned char* pBase = static_cast(vertexBuffer->lock(HardwareBuffer::HBL_DISCARD)); - Real scaleAdjust = 1.f / (size + 2 * border); - Real pixelUV = 2.f * scaleAdjust; - Real uvScale = float(size) * scaleAdjust * float(size - 1) / float(size); + // Calculate tile's position in the virtual cubemap + Real tileSize = 2.0 / (lod + 1); + Real borderSize = (Real(border) / size) * tileSize; + Real left = -1.f + tileSize * x - borderSize; + Real bottom = -1.f + tileSize * y - borderSize; + Real right = left + tileSize + borderSize * 2.0; + Real top = bottom + tileSize + borderSize * 2.0; - Matrix3 faceTransform = PlanetCube::getFaceTransform(face, false); - // Draw a full size quad. for (int y = 0; y < 2; ++y) { for (int x = 0; x < 2; ++x) { @@ -91,22 +94,22 @@ namespace NFSpace { texelem1->baseVertexPointerToElement(pBase, &pTex1); texelem2->baseVertexPointerToElement(pBase, &pTex2); - Vector3 point = Vector3(x ? 1.0f : -1.0f, y ? 1.0f : -1.0f, 1.0f); + // Mystery handedness change, invert y. + Vector3 point = Vector3(x ? -1.0f : 1.0f, y ? 1.0f :-1.0f, -1.0f); - *pTex2++ = point.x; - *pTex2++ = point.y; + *pTex1++ = x ? 0.0f : 1.0f; + *pTex1++ = y ? 0.0f : 1.0f; - point = faceTransform * point; + *pTex2++ = x ? left : right; + *pTex2++ = y ? bottom : top; + + //point = faceTransform * point; + //point = point * uvScale; *pPos++ = point.x; *pPos++ = point.y; *pPos++ = point.z; - point = point * uvScale; - *pTex1++ = -point.x; - *pTex1++ = -point.y; - *pTex1++ = -point.z; - pBase += vertexBuffer->getVertexSize(); } } diff --git a/Source/Planet/Map/PlanetFilter.h b/Source/Planet/Map/PlanetFilter.h index aec367f..3055b9d 100644 --- a/Source/Planet/Map/PlanetFilter.h +++ b/Source/Planet/Map/PlanetFilter.h @@ -20,14 +20,14 @@ namespace NFSpace { class PlanetFilter : public SimpleRenderable { VertexData* sVertexData; - void initVertexData(int face, int size, int border); - void initRenderOp(int face); + void initVertexData(int lod, int x, int y, int size, int border); + void initRenderOp(); public: enum { FILTER_NORMAL_MAP = 1, }; - PlanetFilter(int face, int size, int border); + PlanetFilter(int face, int lod, int x, int y, int size, int border); ~PlanetFilter(); virtual Real getBoundingRadius() const; diff --git a/Source/Planet/Map/PlanetMap.cpp b/Source/Planet/Map/PlanetMap.cpp index 8328a25..be3c201 100644 --- a/Source/Planet/Map/PlanetMap.cpp +++ b/Source/Planet/Map/PlanetMap.cpp @@ -13,26 +13,19 @@ #include "Utility.h" namespace NFSpace { - -const Real PlanetMap::PLANET_TEXTURE_SIZE = 1025; -PlanetMap::PlanetMap() { - // Generate heightmap. +const Real PlanetMap::PLANET_TEXTURE_SIZE = 257; + +PlanetMap::PlanetMap(PlanetDescriptor* descriptor) : mDescriptor(descriptor) { initHelperScene(); - generateHeightMap(); - generateNormalMap(); + initBuffers(); + prepareHeightMap(); } PlanetMap::~PlanetMap() { - if (mSceneManager) { - Root::getSingleton().destroySceneManager(mSceneManager); - } - if (mHeightMap) { - delete mHeightMap; - } - if (mNormalMap) { - delete mNormalMap; - } + deleteHeightMap(); + deleteBuffers(); + deleteHelperScene(); } void PlanetMap::initHelperScene() { @@ -43,100 +36,75 @@ void PlanetMap::initHelperScene() { mCamera->setNearClipDistance(0.01); } -void PlanetMap::generateHeightMap() { - Vector3 position, rand, up; - - SceneNode* brushesNode = mSceneManager->createSceneNode("brushes"); +void PlanetMap::deleteHelperScene() { + if (mSceneManager) { + Root::getSingleton().destroySceneManager(mSceneManager); + } +} - // Draw N random brushes. - srand(getInt("planet.seed")); - for (int i = 0; i < getInt("planet.brushes"); ++i) { - position = Vector3((randf() * 2 - 1), (randf() * 2 - 1), (randf() * 2 - 1)); - position.normalise(); - up = Vector3(randf() * 2 - 1, randf() * 2 - 1, randf() * 2 - 1); - float scale = randf() * .95 + .05; - drawBrush(brushesNode, position, Vector2(scale, scale * (randf() + .5)), up); +void PlanetMap::initBuffers() { + for (int i = 0; i < 2; ++i) { + mMapBuffer[i] = new PlanetMapBuffer(mSceneManager, + mCamera, + PlanetMapBuffer::MAP_TYPE_MONO, + PlanetMap::PLANET_TEXTURE_SIZE, + 1, + 0.5f); } +} + +void PlanetMap::swapBuffers() { + PlanetMapBuffer* temp = mMapBuffer[1]; + mMapBuffer[1] = mMapBuffer[0]; + mMapBuffer[0] = temp; +} - /* - float scale = 0.45; - drawBrush(brushesNode, Vector3( 1.0, 0.0, 0.0), Vector2(scale, scale), Vector3(0.0, 1.0, 0.0)); - drawBrush(brushesNode, Vector3(-1.0, 0.0, 0.0), Vector2(scale, scale), Vector3(0.0, 1.0, 0.0)); - drawBrush(brushesNode, Vector3(0.0, 1.0, 0.0), Vector2(scale, scale), Vector3(0.0, 0.0, 1.0)); - drawBrush(brushesNode, Vector3(0.0,-1.0, 0.0), Vector2(scale, scale), Vector3(0.0, 0.0, 1.0)); - drawBrush(brushesNode, Vector3(0.0, 0.0, 1.0), Vector2(scale, scale), Vector3(1.0, 0.0, 0.0)); - drawBrush(brushesNode, Vector3(0.0, 0.0,-1.0), Vector2(scale, scale), Vector3(1.0, 0.0, 0.0)); - */ - - mHeightMap = new PlanetMapBuffer(mSceneManager, - mCamera, - PlanetMapBuffer::MAP_TYPE_MONO, - PlanetMap::PLANET_TEXTURE_SIZE, - 0, - 0.5f); - mHeightMap->render(brushesNode); - mHeightMap->save(false); - - SceneNode::ObjectIterator it = brushesNode->getAttachedObjectIterator(); - while (it.hasMoreElements()) { - delete it.getNext(); +void PlanetMap::deleteBuffers() { + for (int i = 0; i < 2; ++i) { + delete mMapBuffer[i]; } - brushesNode->detachAllObjects(); - - mSceneManager->destroySceneNode(brushesNode); } +void PlanetMap::prepareHeightMap() { + mHeightMapBrushes = mSceneManager->createSceneNode("heightMapBrushes"); + + Vector3 position, rand, up; -void PlanetMap::generateNormalMap() { - // Create new buffer for the normal map. - mNormalMap = new PlanetMapBuffer(mSceneManager, - mCamera, - PlanetMapBuffer::MAP_TYPE_NORMAL, - PlanetMap::PLANET_TEXTURE_SIZE, - 0, - 1.f); - - // Prepare texture substitution list. - AliasTextureNamePairList AliasList; - AliasList.insert(AliasTextureNamePairList::value_type("source", mHeightMap->getTextureName())); - - // Alter the material to use the height map as its source texture. - MaterialPtr normalMapperMaterial; - normalMapperMaterial = MaterialManager::getSingleton().getByName("Planet/NormalMapper"); - normalMapperMaterial->applyTextureAliases(AliasList); + // TODO: run real script w/ real descriptor - // Create scene node to hold all the renderables. - SceneNode* filterNode = mSceneManager->createSceneNode("filterSet"); - - // Create 6 filter faces to represent the cubemap environment. - for (int face = 0; face < 6; ++face) { - PlanetFilter *filter = new PlanetFilter(face, PlanetMap::PLANET_TEXTURE_SIZE, 0); - filter->setMaterial("Planet/NormalMapper"); - filterNode->attachObject(filter); + // Draw N random brushes. + srand(mDescriptor->seed); + for (int i = 0; i < mDescriptor->brushes; ++i) { + position = Vector3((randf() * 2 - 1), (randf() * 2 - 1), (randf() * 2 - 1)); + position.normalise(); + up = Vector3(randf() * 2 - 1, randf() * 2 - 1, randf() * 2 - 1); + float scale = randf() * .95 + .05; + drawBrush(mHeightMapBrushes, position, Vector2(scale, scale * (randf() + .5)), up); } - // Render the scene to the new map. - mNormalMap->render(filterNode); - mNormalMap->save(false); +} - // Clean-up the renderables and detach them. - SceneNode::ObjectIterator it = filterNode->getAttachedObjectIterator(); +void PlanetMap::deleteHeightMap() { + SceneNode::ObjectIterator it = mHeightMapBrushes->getAttachedObjectIterator(); while (it.hasMoreElements()) { delete it.getNext(); } - filterNode->detachAllObjects(); + mHeightMapBrushes->detachAllObjects(); - // Destroy the scene node. - mSceneManager->destroySceneNode(filterNode); + mSceneManager->destroySceneNode(mHeightMapBrushes); } -Image* PlanetMap::getHeightMap(int face) { - return mHeightMap->getFace(face); -} - -std::string PlanetMap::getMaterial() { -// return mHeightMap->getMaterial(); - return mNormalMap->getMaterial(); +PlanetMapTile* PlanetMap::generateTile(int face, int lod, int x, int y) { + // Generate height texture and load into system memory for analysis. + mMapBuffer[FRONT]->render(face, lod, x, y, mHeightMapBrushes); + TexturePtr heightTexture = mMapBuffer[FRONT]->saveTexture(false); + Image heightImage = mMapBuffer[FRONT]->saveImage(false); + + // Generate normal map based on heightmap texture. + mMapBuffer[BACK]->filter(face, lod, x, y, PlanetMapBuffer::FILTER_TYPE_NORMAL, mMapBuffer[FRONT]); + TexturePtr normalTexture = mMapBuffer[BACK]->saveTexture(false); + + return new PlanetMapTile(heightTexture, heightImage, normalTexture, PlanetMap::PLANET_TEXTURE_SIZE); } void PlanetMap::drawBrush(SceneNode* brushesNode, Vector3 position, Vector2 scale, Vector3 up) { diff --git a/Source/Planet/Map/PlanetMap.h b/Source/Planet/Map/PlanetMap.h index 4ea1ed3..a44d2be 100644 --- a/Source/Planet/Map/PlanetMap.h +++ b/Source/Planet/Map/PlanetMap.h @@ -13,40 +13,52 @@ #include #include +#include "PlanetDescriptor.h" #include "PlanetBrush.h" #include "PlanetFilter.h" #include "PlanetMapBuffer.h" +#include "PlanetMapTile.h" using namespace Ogre; namespace NFSpace { /** - * Controller for creating the surface map for a planet. + * Generator for creating surface tiles for planets. */ class PlanetMap { public: static const Real PLANET_TEXTURE_SIZE; - PlanetMap(); + enum { + FRONT, + BACK + }; + + PlanetMap(PlanetDescriptor* descriptor); ~PlanetMap(); - Image* getHeightMap(int face); - - std::string getMaterial(); - void drawBrush(SceneNode* brushesNode, Vector3 position, Vector2 scale, Vector3 up); + PlanetMapTile* generateTile(int face, int lod, int x, int y); protected: void initHelperScene(); - void generateHeightMap(); - void generateNormalMap(); + void deleteHelperScene(); + + void initBuffers(); + void swapBuffers(); + void deleteBuffers(); + + void prepareHeightMap(); + void deleteHeightMap(); - SceneManager *mSceneManager; - Camera *mCamera; + PlanetDescriptor* mDescriptor; + SceneManager* mSceneManager; + Camera* mCamera; + + SceneNode* mHeightMapBrushes; - PlanetMapBuffer *mHeightMap; - PlanetMapBuffer *mNormalMap; + PlanetMapBuffer* mMapBuffer[2]; }; }; diff --git a/Source/Planet/Map/PlanetMapBuffer.cpp b/Source/Planet/Map/PlanetMapBuffer.cpp index eaf3afb..5089f85 100644 --- a/Source/Planet/Map/PlanetMapBuffer.cpp +++ b/Source/Planet/Map/PlanetMapBuffer.cpp @@ -17,78 +17,37 @@ namespace NFSpace { -const unsigned int PlanetMapBuffer::LEVEL_SHIFT = 0; -const unsigned int PlanetMapBuffer::LEVEL_MASK = 0xFFFF; const unsigned int PlanetMapBuffer::LEVEL_MIN = 0; -const unsigned int PlanetMapBuffer::LEVEL_MID = 0x8000; -const unsigned int PlanetMapBuffer::LEVEL_MAX = 0xFFFF; const unsigned int PlanetMapBuffer::LEVEL_RANGE = 1; - + PlanetMapBuffer::PlanetMapBuffer(SceneManager* sceneManager, Camera* camera, int type, int size, int border, Real fill) -: mSize(size), mBorder(border), mType(type), mFill(fill), mSceneManager(sceneManager), mCamera(camera), mMaterialCreated(false) { +: mSize(size), mBorder(border), mType(type), mFill(fill), mSceneManager(sceneManager), mCamera(camera) { assert(isPowerOf2(size - 1)); mFullSize = mSize + 2 * mBorder; init(); } PlanetMapBuffer::~PlanetMapBuffer() { - if (mBorder) { - // Dilated cubemap - for (int i = 0; i < 6; ++i) { - TextureManager::getSingleton().remove(mTextureFaces[i]->getName()); - } - } - else { - TextureManager::getSingleton().remove(mTexture->getName()); - } - - if (mMaterialCreated) { - MaterialManager::getSingleton().remove(mMaterial->getName()); - } + TextureManager::getSingleton().remove(mTexture->getName()); } void PlanetMapBuffer::init() { - if (mBorder) { - // Dilated cubemap - for (int i = 0; i < 6; ++i) { - mTextureFaces[i] = TextureManager::getSingleton().createManual( - getUniqueId("Buffer"), // Name of texture - "PlanetMap", // Name of resource group in which the texture should be created - TEX_TYPE_2D, // Texture type - mFullSize, // Width - mFullSize, // Height - 1, // Depth (Must be 1 for two dimensional textures) - 0, // Number of mipmaps - getPixelFormat(), // Pixel format - TU_RENDERTARGET // usage - ); - } - for (int i = 0; i < 6; ++i) { - mRenderTexture[i] = mTextureFaces[i]->getBuffer()->getRenderTarget(); - mRenderTexture[i]->setAutoUpdated(FALSE); - } - } - else { - // Regular cubemap - mTexture = TextureManager::getSingleton().createManual( - getUniqueId("Buffer"), // Name of texture - "PlanetMap", // Name of resource group in which the texture should be created - TEX_TYPE_CUBE_MAP, // Texture type - mFullSize, // Width - mFullSize, // Height - 1, // Depth (Must be 1 for two dimensional textures) - 0, // Number of mipmaps - getPixelFormat(), // Pixel format - TU_RENDERTARGET // usage - ); - for (int i = 0; i < 6; ++i) { - mRenderTexture[i] = mTexture->getBuffer(i)->getRenderTarget(); - mRenderTexture[i]->setAutoUpdated(FALSE); - } - } + mTexture = TextureManager::getSingleton().createManual( + getUniqueId("Buffer"), // Name of texture + "PlanetMap", // Name of resource group in which the texture should be created + TEX_TYPE_2D, // Texture type + mFullSize, // Width + mFullSize, // Height + 1, // Depth (Must be 1 for two dimensional textures) + 0, // Number of mipmaps + getPixelFormat(), // Pixel format + TU_RENDERTARGET // usage + ); + mRenderTexture = mTexture->getBuffer()->getRenderTarget(); + mRenderTexture->setAutoUpdated(FALSE); } -void PlanetMapBuffer::render(SceneNode* brushes) { +void PlanetMapBuffer::render(int face, int lod, int x, int y, SceneNode* brushes) { // Add brushes into the scene. SceneNode* node = mSceneManager->getRootSceneNode()->createChildSceneNode(); node->addChild(brushes); @@ -96,156 +55,92 @@ void PlanetMapBuffer::render(SceneNode* brushes) { //DumpScene(mSceneManager); // Render each cube face from the scene graph. - for (int i = 0; i < 6; ++i) { - renderFace(i, true, FBT_COLOUR | FBT_DEPTH); - } + renderTile(face, lod, x, y, true, FBT_COLOUR | FBT_DEPTH); // Remove brushes. node->removeChild(brushes); mSceneManager->getRootSceneNode()->removeChild(node); mSceneManager->destroySceneNode(node); - - edgeFixup(); - prepareMaterial(); } -void PlanetMapBuffer::renderFace(int face, bool transform, unsigned int clearFrame) { - // Set camera to have a perfect 45deg FOV in the non-border area. - mCamera->setFOVy(Radian(atan(float(mFullSize + 1) / (mSize)) * 2)); - while (mRenderTexture[face]->getNumViewports() > 0) { - mRenderTexture[face]->removeViewport(0); - } - mRenderTexture[face]->addViewport(mCamera); - mRenderTexture[face]->getViewport(0)->setClearEveryFrame((bool)clearFrame, clearFrame); - mRenderTexture[face]->getViewport(0)->setBackgroundColour(ColourValue(mFill, mFill, mFill, 1)); - mRenderTexture[face]->getViewport(0)->setOverlaysEnabled(false); +void PlanetMapBuffer::filter(int face, int lod, int x, int y, int type, PlanetMapBuffer* source) { + if (type != FILTER_TYPE_NORMAL) return; + + // Prepare texture substitution list. + AliasTextureNamePairList aliasList; + aliasList.insert(AliasTextureNamePairList::value_type("source", source->getTextureName())); - // Set camera to cube face transformation. - // Note: cubemap space is left-handed when viewed from inside the cube. - // Flip the basis vector's x coordinates to compensate. - if (transform) { - Matrix3 faceMatrix = PlanetCube::getFaceTransform(face, false); - Quaternion orientation = Quaternion(faceMatrix); - mCamera->setOrientation(orientation); - } - else { - mCamera->setOrientation(Quaternion(1.f, 0.f, 0.f, 0.f)); - } - // Render the frame to the texture. - mRenderTexture[face]->update(); -} + // Alter the material to use the height map as its source texture. + MaterialPtr normalMapperMaterial; + normalMapperMaterial = MaterialManager::getSingleton().getByName("Planet/NormalMapper"); + normalMapperMaterial->applyTextureAliases(aliasList); -void PlanetMapBuffer::edgeFixup() { - if (mBorder) { - // Prepare materials for rendering the cube face edges. - for (int i = 0; i < 6; ++i) { - mMaterialFaces[i] = MaterialManager::getSingleton().create( - mTextureFaces[i]->getName(), // name - "PlanetMap"); - - Pass* pass = mMaterialFaces[i]->getTechnique(0)->getPass(0); - pass->setCullingMode(CULL_NONE); - pass->setSceneBlending(SBT_ADD); - pass->setDepthCheckEnabled(true); - pass->setDepthWriteEnabled(true); - - TextureUnitState *textureUnit = pass->createTextureUnitState(mTexture->getName()); - textureUnit->setTextureAddressingMode(TextureUnitState::TAM_CLAMP); - textureUnit->setTextureFiltering(TFO_NONE); - } - - // Add fix-up node. - SceneNode* node = mSceneManager->getRootSceneNode()->createChildSceneNode(); - - // Fix up each cube face using fixup edges in the scene graph. - for (int i = 0; i < 6; ++i) { - PlanetEdgeFixup* edgeFixup[4]; - for (int j = 0; j < 4; ++j) { - edgeFixup[j] = new PlanetEdgeFixup(i, j, mSize, mBorder, PlanetEdgeFixup::FIX_BORDER, mMaterialFaces); - node->attachObject(edgeFixup[j]); - } - - // Update face - renderFace(i, false, false); - - // Clean-up - for (int j = 0; j < 4; ++j) { - node->detachObject(edgeFixup[j]); - delete edgeFixup[j]; - } + // Create scene node to hold all the renderables. + SceneNode* filterNode = mSceneManager->getRootSceneNode()->createChildSceneNode(); + + // Create a filter face (fullscreen quad). + PlanetFilter *filter = new PlanetFilter(face, lod, x, y, mSize, mBorder); + filter->setMaterial("Planet/NormalMapper"); + filterNode->attachObject(filter); + + renderTile(face, lod, x, y, false, true); - } - - // Clean-up - mSceneManager->getRootSceneNode()->removeChild(node); - mSceneManager->destroySceneNode(node); + // Clean-up the renderables and detach them. + SceneNode::ObjectIterator it = filterNode->getAttachedObjectIterator(); + while (it.hasMoreElements()) { + delete it.getNext(); } - -} + filterNode->detachAllObjects(); -void PlanetMapBuffer::prepareMaterial() { - mMaterialCreated = TRUE; + // Destroy the scene node. + mSceneManager->getRootSceneNode()->removeChild(filterNode); + mSceneManager->destroySceneNode(filterNode); +} - // Prepare material for cube mapping. - mMaterial = MaterialManager::getSingleton().create( - mTexture->getName() + "Material", // name - "PlanetMap"); - Pass* pass = mMaterial->getTechnique(0)->getPass(0); - pass->setSceneBlending(SBT_REPLACE); - pass->setCullingMode(CULL_CLOCKWISE); - pass->setLightingEnabled(FALSE); - pass->setDepthWriteEnabled(TRUE); - pass->setDepthCheckEnabled(TRUE); +TexturePtr PlanetMapBuffer::saveTexture(bool border) { + // Alloc write once texture at right size + int size = border ? mFullSize : mSize; + int edge = border ? 0 : mBorder ; + TexturePtr texture = TextureManager::getSingleton().createManual( + getUniqueId("Tile"), // Name of texture + "PlanetMap", // Name of resource group in which the texture should be created + TEX_TYPE_2D, // Texture type + size, // Width + size, // Height + 1, // Depth (Must be 1 for two dimensional textures) + 0, // Number of mipmaps + getPixelFormat(), // Pixel format + TU_STATIC_WRITE_ONLY // usage + ); + // Blit current front buffer contents into new texture. + texture->getBuffer()->blit(mTexture->getBuffer(), + Box(edge, edge, 0, size + edge, size + edge, 1), + Box(0, 0, 0, size, size, 1) + ); - TextureUnitState* textureUnit = pass->createTextureUnitState(); - textureUnit->setTextureName(mTexture->getName(), TEX_TYPE_CUBE_MAP); - textureUnit->setColourOperation(LBO_REPLACE); - textureUnit->setTextureFiltering(FO_LINEAR, FO_LINEAR, FO_NONE); - textureUnit->setTextureAddressingMode(TextureUnitState::TAM_CLAMP); + return texture; } -void PlanetMapBuffer::save(bool border, bool file) { - std::string names[] = { - "rt", - "lf", - "up", - "dn", - "fr", - "bk", - }; +Image PlanetMapBuffer::saveImage(bool border) { + // Create system memory buffer to hold pixel data. + PixelFormat pf = getPixelFormat(); - for (int i = 0; i < 6; ++i) { - // Create system memory buffer to hold pixel data. - PixelFormat pf = getPixelFormat(); - - uchar *data = OGRE_ALLOC_T(uchar, mRenderTexture[i]->getWidth() * mRenderTexture[i]->getHeight() * PixelUtil::getNumElemBytes(pf), MEMCATEGORY_GENERAL); - PixelBox pb(mRenderTexture[i]->getWidth(), mRenderTexture[i]->getHeight(), 1, pf, data); - - // Load data and create an image object. - mRenderTexture[i]->copyContentsToMemory(pb, RenderTarget::FB_AUTO); - mImage[i] = Image().loadDynamicImage(data, mRenderTexture[i]->getWidth(), mRenderTexture[i]->getHeight(), 1, pf, true, 1, 0); - - // Save as file - if (file) { - mRenderTexture[i]->writeContentsToFile("cube-" + mTexture->getName() + "-" + names[i] + ".png"); - } - - // Crop image if needed. - if (mBorder && !border) { - Image cropped; - cropped = cropImage(mImage[i], mBorder, mBorder, mSize, mSize); - OGRE_FREE(mImage[i].getData(), MEMCATEGORY_RENDERSYS); - mImage[i] = cropped; - } + uchar *data = OGRE_ALLOC_T(uchar, mRenderTexture->getWidth() * mRenderTexture->getHeight() * PixelUtil::getNumElemBytes(pf), MEMCATEGORY_GENERAL); + PixelBox pb(mRenderTexture->getWidth(), mRenderTexture->getHeight(), 1, pf, data); + + // Load data and create an image object. + mRenderTexture->copyContentsToMemory(pb, RenderTarget::FB_AUTO); + Image image = Image().loadDynamicImage(data, mRenderTexture->getWidth(), mRenderTexture->getHeight(), 1, pf, true, 1, 0); + + // Crop image if needed. + if (mBorder && !border) { + Image cropped; + cropped = cropImage(image, mBorder, mBorder, mSize, mSize); +// OGRE_FREE(image.getData(), MEMCATEGORY_RENDERSYS); + image = cropped; } -} - -Image* PlanetMapBuffer::getFace(int face) { - return &mImage[face]; -} -std::string PlanetMapBuffer::getMaterial() { - return mMaterial->getName();//"Planet/Surface";//mMaterial->getName(); + return image; } PixelFormat PlanetMapBuffer::getPixelFormat() { @@ -262,5 +157,47 @@ String PlanetMapBuffer::getTextureName() { return mTexture->getName(); } +void PlanetMapBuffer::renderTile(int face, int lod, int x, int y, bool transform, unsigned int clearFrame) { + // Ensure viewport is set up correctly. + while (mRenderTexture->getNumViewports() > 0) { + mRenderTexture->removeViewport(0); + } + mRenderTexture->addViewport(mCamera); + mRenderTexture->getViewport(0)->setClearEveryFrame((bool)clearFrame, clearFrame); + mRenderTexture->getViewport(0)->setBackgroundColour(ColourValue(mFill, mFill, mFill, 1)); + mRenderTexture->getViewport(0)->setOverlaysEnabled(false); + + if (transform) { + // Set camera to have a perfect 45deg FOV in the non-border area. + mCamera->setFOVy(Radian(atan(float(mFullSize + 1) / (mSize)) * 2)); + + // Set camera to cube face transformation. + // Note: cubemap space is left-handed when viewed from inside the cube. + // Flip the basis vector's x coordinates to compensate. + + Quaternion orientation = PlanetCube::getFaceCamera(face); + mCamera->setOrientation(orientation); + } + else { + // Viewport/source is pre-transformed. + mCamera->setFOVy(Radian(atan(1) * 2)); + mCamera->setOrientation(Quaternion(1.f, 0.f, 0.f, 0.f)); + } + + // Now skew the projection matrix. + + /* + Ogre::Matrix4 m; + m = m_camera->getProjectionMatrix(); + m[0][2]=-2*Ogre::Math::Cos(Ogre::Degree(45))/m_camera->getOrthoWindowWidth(); + m[1][2]=-2*Ogre::Math::Sin(Ogre::Degree(45))/m_camera->getOrthoWindowHeight(); + m_camera->setCustomProjectionMatrix(true,m); + */ + + + // Render the frame to the texture. + mRenderTexture->update(); +} + } diff --git a/Source/Planet/Map/PlanetMapBuffer.h b/Source/Planet/Map/PlanetMapBuffer.h index bc81774..181a0e0 100644 --- a/Source/Planet/Map/PlanetMapBuffer.h +++ b/Source/Planet/Map/PlanetMapBuffer.h @@ -17,11 +17,11 @@ using namespace Ogre; namespace NFSpace { typedef unsigned short HeightMapPixel[4]; - + /** - * Data structure for creating, loading and storing a base level cube-map for a planet surface. + * Working buffer for creating maps for a planet surface. * - * Implements drawing operations into itself using the provided scenemanager and camera. + * Executes drawing operations into itself using the provided scenemanager and camera. */ class PlanetMapBuffer { int mSize; @@ -34,29 +34,25 @@ namespace NFSpace { Camera* mCamera; public: - static const unsigned int LEVEL_SHIFT; - static const unsigned int LEVEL_MASK; static const unsigned int LEVEL_MIN; - static const unsigned int LEVEL_MID; - static const unsigned int LEVEL_MAX; static const unsigned int LEVEL_RANGE; enum { MAP_TYPE_MONO, MAP_TYPE_NORMAL, }; - + enum { - + FILTER_TYPE_NORMAL, }; PlanetMapBuffer(SceneManager* sceneManager, Camera* camera, int type, int size, int border, Real fill); ~PlanetMapBuffer(); - - Image* getFace(int face); - void render(SceneNode* brushes); - void edgeFixup(); - void save(bool border, bool file = false); + + void render(int face, int lod, int x, int y, SceneNode* brushes); + void filter(int face, int lod, int x, int y, int type, PlanetMapBuffer* source); + TexturePtr saveTexture(bool border); + Image saveImage(bool border); void prepareMaterial(); std::string getMaterial(); @@ -65,16 +61,11 @@ namespace NFSpace { protected: void init(); - void renderFace(int face, bool transform, unsigned int clearFrame); + void renderTile(int face, int lod, int x, int y, bool transform, unsigned int clearFrame); PixelFormat getPixelFormat(); - MaterialPtr mMaterialFaces[6]; - MaterialPtr mMaterial; - bool mMaterialCreated; TexturePtr mTexture; - TexturePtr mTextureFaces[6]; - RenderTexture* mRenderTexture[6]; - Image mImage[6]; + RenderTexture* mRenderTexture; }; }; diff --git a/Source/Planet/Mesh/PlanetCube.cpp b/Source/Planet/Mesh/PlanetCube.cpp index ba97cd8..a2feed6 100644 --- a/Source/Planet/Mesh/PlanetCube.cpp +++ b/Source/Planet/Mesh/PlanetCube.cpp @@ -40,9 +40,11 @@ void PlanetCube::deleteFace(int face) { } void PlanetCube::initQuadTreeNode(QuadTreeNode* node) { - node->createRenderable(mMap->getHeightMap(node->mFace)); + node->createMapTile(mMap); + node->createRenderable(node->mMapTile->getHeightMap()); node->mRenderable->setProxy(mProxy); - node->mRenderable->setMaterial(mMap->getMaterial()); + node->mRenderable->setMaterial(node->mMapTile->getMaterial()); + if (node->mLOD < getInt("planet.lodLimit")) { for (int i = 0; i < 4; ++i) { QuadTreeNode* child = new QuadTreeNode(); @@ -84,26 +86,39 @@ const Real PlanetCube::getScale() const { return getReal("planet.radius") + getReal("planet.height"); } -const Matrix3 PlanetCube::getFaceTransform(int face, bool lhs) { - if (!lhs) { - Matrix3 faceTransform = getFaceTransform(face, true); - faceTransform = faceTransform * Matrix3(-1, 0, 0, - 0, 1, 0, - 0, 0, 1); - return faceTransform; +const Quaternion PlanetCube::getFaceCamera(int face) { + // The camera is looking at the specified planet face from the inside. + switch (face) { + default: + case Planet::RIGHT: + return Quaternion(0.707f, 0.0f,-0.707f, 0.0f); + case Planet::LEFT: + return Quaternion(0.707f, 0.0f, 0.707f, 0.0f); + case Planet::TOP: + return Quaternion(0.707f,-0.707f, 0.0f, 0.0f); + case Planet::BOTTOM: + return Quaternion(0.707f, 0.707f, 0.0f, 0.0f); + case Planet::FRONT: + return Quaternion(0.0f, 0.0f, 1.0f, 0.0f); + case Planet::BACK: + return Quaternion(1.0f, 0.0f, 0.0f, 0.0f); } - // Note, these are LHS transforms due to following the renderman convention of texture mapping a cubemap. +} + +const Matrix3 PlanetCube::getFaceTransform(int face) { + // Note, these are LHS transforms because the cube is rendered from the inside, but seen from the outside. + // Hence there needs to be a parity switch for each face. switch (face) { default: case Planet::RIGHT: return Matrix3( 0, 0, 1, - 0,-1, 0, - -1, 0, 0 + 0, 1, 0, + 1, 0, 0 ); case Planet::LEFT: return Matrix3( 0, 0,-1, - 0,-1, 0, - 1, 0, 0 + 0, 1, 0, + -1, 0, 0 ); case Planet::TOP: return Matrix3( 1, 0, 0, @@ -116,13 +131,13 @@ const Matrix3 PlanetCube::getFaceTransform(int face, bool lhs) { 0,-1, 0 ); case Planet::FRONT: - return Matrix3( 1, 0, 0, - 0,-1, 0, + return Matrix3(-1, 0, 0, + 0, 1, 0, 0, 0, 1 ); case Planet::BACK: - return Matrix3(-1, 0, 0, - 0,-1, 0, + return Matrix3( 1, 0, 0, + 0, 1, 0, 0, 0,-1 ); } diff --git a/Source/Planet/Mesh/PlanetCube.h b/Source/Planet/Mesh/PlanetCube.h index 52da143..75c25a6 100644 --- a/Source/Planet/Mesh/PlanetCube.h +++ b/Source/Planet/Mesh/PlanetCube.h @@ -15,9 +15,8 @@ class PlanetCube; #include #include "Planet.h" -#include "PlanetCubeTree.h" - #include "PlanetMap.h" +#include "PlanetCubeTree.h" using namespace Ogre; @@ -36,12 +35,13 @@ class PlanetCube { PlanetCube(MovableObject* proxy, PlanetMap* map); ~PlanetCube(); - static const Matrix3 getFaceTransform(int face, bool lhs); + static const Quaternion PlanetCube::getFaceCamera(int face); + static const Matrix3 getFaceTransform(int face); const Real getScale() const; virtual void updateRenderQueue(RenderQueue* queue, const Matrix4& fullTransform); void setCamera(Camera* camera); - + Renderable* getRenderableForNode(QuadTreeNode* node); protected: void initQuadTreeNode(QuadTreeNode* node); diff --git a/Source/Planet/Mesh/PlanetCubeTree.cpp b/Source/Planet/Mesh/PlanetCubeTree.cpp index 0eff11e..19674c9 100644 --- a/Source/Planet/Mesh/PlanetCubeTree.cpp +++ b/Source/Planet/Mesh/PlanetCubeTree.cpp @@ -8,12 +8,14 @@ */ #include "PlanetCubeTree.h" +#include "EngineState.h" namespace NFSpace { QuadTreeNode::QuadTreeNode() : mRenderable(0), +mMapTile(0), mParent(0), mParentSlot(-1), mFace(0), @@ -29,6 +31,7 @@ QuadTreeNode::~QuadTreeNode() { if (mParent) { mParent->mChildren[mParentSlot] = 0; } + destroyMapTile(); destroyRenderable(); for (int i = 0; i < 4; ++i) { detachChild(i); @@ -56,6 +59,16 @@ void QuadTreeNode::propagateLODDistances() { } } +void QuadTreeNode::createMapTile(PlanetMap* map) { + if (mMapTile) throw "Creating map tile that already exists."; + mMapTile = map->generateTile(mFace, mLOD, mX, mY); +} + +void QuadTreeNode::destroyMapTile() { + if (mMapTile) delete mMapTile; + mMapTile = 0; +} + void QuadTreeNode::createRenderable(Image* map) { if (mRenderable) throw "Creating renderable that already exists."; mRenderable = new PlanetRenderable(this, map); @@ -95,7 +108,44 @@ void QuadTreeNode::render(RenderQueue* queue, int lodLimit, SimpleFrustum& frust return; } if (mLOD == lodLimit || mRenderable->isInLODRange()) { - queue->addRenderable(mRenderable); + Vector3 positionOffset = cameraPosition - mRenderable->getCenter(); + float distance = positionOffset.length(); + Vector3 viewDirection = positionOffset; + viewDirection.normalise(); + float size = getReal("planet.radius") * Math::PI / 2; + float res = size / PlanetMap::PLANET_TEXTURE_SIZE; + float screenres = distance / getInt("screenWidth"); + Real lodSpan = getReal("planet.radius") / ((1 << mLOD) * distance); + Real lodShorten = minf(1.0f, mRenderable->mSurfaceNormal.dotProduct(viewDirection) + lodSpan); + + res *= lodShorten; +/* + if (res > screenres) { + if (res / 2 > screenres) { + if (res / 4 > screenres) { + if (res / 8 > screenres) { + mRenderable->setCustomParameter(9, Vector4(0, 0, 1, 1)); + } + else { + mRenderable->setCustomParameter(9, Vector4(0, 0.7, 0, 1)); + } + } + else { + mRenderable->setCustomParameter(9, Vector4(1, 0, 0, 1)); + } + } + else { + mRenderable->setCustomParameter(9, Vector4(1, 1, 0, 1)); + } + } + else { + mRenderable->setCustomParameter(9, Vector4(1, 1, 1, 1)); + } +*/ + mRenderable->updateRenderQueue(queue); + + +// queue->addRenderable(mRenderable); return; } } diff --git a/Source/Planet/Mesh/PlanetCubeTree.h b/Source/Planet/Mesh/PlanetCubeTree.h index dff36f9..2424d71 100644 --- a/Source/Planet/Mesh/PlanetCubeTree.h +++ b/Source/Planet/Mesh/PlanetCubeTree.h @@ -32,6 +32,8 @@ struct QuadTreeNode { QuadTreeNode(); ~QuadTreeNode(); void propagateLODDistances(); + void createMapTile(PlanetMap* map); + void destroyMapTile(); void createRenderable(Image* map); void destroyRenderable(); void attachChild(QuadTreeNode* child, int position); @@ -42,6 +44,8 @@ struct QuadTreeNode { int mLOD; int mX; int mY; + + PlanetMapTile* mMapTile; PlanetRenderable* mRenderable; int mParentSlot; diff --git a/Source/Planet/Mesh/PlanetRenderable.cpp b/Source/Planet/Mesh/PlanetRenderable.cpp index c9259ce..bdf2122 100644 --- a/Source/Planet/Mesh/PlanetRenderable.cpp +++ b/Source/Planet/Mesh/PlanetRenderable.cpp @@ -32,7 +32,7 @@ VertexDeclaration *PlanetRenderable::sVertexDeclaration; * Constructor. */ PlanetRenderable::PlanetRenderable(QuadTreeNode* node, Image* map) -: mProxy(0), mQuadTreeNode(node), mMap(map), mChildDistance(0), mChildDistanceSquared(0) { +: mProxy(0), mQuadTreeNode(node), mMap(map), mChildDistance(0), mChildDistanceSquared(0), mWireBoundingBox(0) { mPlanetRadius = getReal("planet.radius"); mPlanetHeight = getReal("planet.height"); @@ -48,11 +48,15 @@ PlanetRenderable::PlanetRenderable(QuadTreeNode* node, Image* map) setMaterial("BaseWhiteNoLighting"); fillHardwareBuffers(); + analyseTerrain(); + initDisplacementMapping(); } PlanetRenderable::~PlanetRenderable() { removeInstance(); - log("~PlanetRenderable"); + if (mWireBoundingBox) { + OGRE_DELETE mWireBoundingBox; + } } Real PlanetRenderable::getLODDistance() { @@ -88,12 +92,6 @@ Real PlanetRenderable::getBoundingRadius() { return mBoundingRadius; } - /* - setCustomParameter(1, Vector4((randf() * .5 - .25), 0, 0, 0)); - setCustomParameter(2, Vector4(0.05, 0, 0, 0)); - setCustomParameter(4, Vector4(randf() * 2.0 - 1.0, randf() * 2.0 - 1.0, 0, 0)); - */ - void PlanetRenderable::addInstance() { if (!sInstances++) { sGridSize = getInt("planet.gridSize"); @@ -113,7 +111,148 @@ void PlanetRenderable::removeInstance() { } } +/** + * Set up the parameters for the vertex shader warp / displacement mapping. + */ +void PlanetRenderable::initDisplacementMapping() { + // Calculate scales, offsets and steps. + const Real scale = (Real)(1 << mQuadTreeNode->mLOD); + const Real invScale = 2.0f / scale; + const Real positionX = -1.f + invScale * mQuadTreeNode->mX; + const Real positionY = -1.f + invScale * mQuadTreeNode->mY; + const int stepX = (mMap->getWidth() - 1) / (1 << mQuadTreeNode->mLOD) / (sGridSize - 1); + const int stepY = (mMap->getHeight() - 1) / (1 << mQuadTreeNode->mLOD) / (sGridSize - 1); + const int pixelX = mQuadTreeNode->mX * stepX * (sGridSize - 1); + const int pixelY = mQuadTreeNode->mY * stepY * (sGridSize - 1); + const int offsetX = stepX; + const int offsetY = stepY * mMap->getWidth(); + + setCustomParameter(1, Vector4(invScale, 0, 0, 0)); + setCustomParameter(2, Vector4(positionX, positionY, 0, 0)); + setCustomParameter(3, Vector4(mPlanetRadius, 0, 0, 0)); + setCustomParameter(4, Vector4(mPlanetHeight, 0, 0, 0)); + setCustomParameter(5, Vector4(mDistance, 0, 0, 0)); + + Matrix3 faceTransform = PlanetCube::getFaceTransform(mQuadTreeNode->mFace); + setCustomParameter(6, Vector4(faceTransform[0][0], faceTransform[1][0], faceTransform[2][0], 0)); + setCustomParameter(7, Vector4(faceTransform[0][1], faceTransform[1][1], faceTransform[2][1], 0)); + setCustomParameter(8, Vector4(faceTransform[0][2], faceTransform[1][2], faceTransform[2][2], 0)); + + setCustomParameter(9, Vector4(1, 1, 1, 1)); + //setCustomParameter(6, Vector4(faceTransform[0][0], faceTransform[0][1], faceTransform[0][2], 0)); + //setCustomParameter(7, Vector4(faceTransform[1][0], faceTransform[1][1], faceTransform[1][2], 0)); + //setCustomParameter(8, Vector4(faceTransform[2][0], faceTransform[2][1], faceTransform[2][2], 0)); +} + +/** + * Analyse the terrain for this tile. + */ +void PlanetRenderable::analyseTerrain() { + // Examine pixel buffer to identify + HeightMapPixel* pMap = (HeightMapPixel*)(mMap->getData()); + + // Calculate scales, offsets and steps. + const Real scale = (Real)(1 << mQuadTreeNode->mLOD); + const Real invScale = 2.0f / scale; + const Real positionX = -1.f + invScale * mQuadTreeNode->mX; + const Real positionY = -1.f + invScale * mQuadTreeNode->mY; + const int stepX = (mMap->getWidth() - 1) / (1 << mQuadTreeNode->mLOD) / (sGridSize - 1); + const int stepY = (mMap->getHeight() - 1) / (1 << mQuadTreeNode->mLOD) / (sGridSize - 1); + const int pixelX = mQuadTreeNode->mX * stepX * (sGridSize - 1); + const int pixelY = mQuadTreeNode->mY * stepY * (sGridSize - 1); + const int offsetX = stepX; + const int offsetY = stepY * mMap->getWidth(); + + HeightMapPixel* pMapCorner = &pMap[pixelY * mMap->getWidth() + pixelX]; + + // Keep track of extents. + Vector3 min = Vector3(1e8), max = Vector3(-1e8); + mCenter = Vector3(0, 0, 0); + + Matrix3 faceTransform = PlanetCube::getFaceTransform(mQuadTreeNode->mFace); + + //#define getOffsetPixel(x) ((float)(((*(pMapRow + (x))) & PlanetMapBuffer::LEVEL_MASK) >> PlanetMapBuffer::LEVEL_SHIFT)) + #define getOffsetPixel(x) Bitwise::halfToFloat(*((unsigned short*)(pMapRow + (x)))) + //#define getOffsetPixel(x) (*((float*)(pMapRow + (x)))) + + // Lossy representation of heightmap + const int offsetX2 = offsetX * 2; + const int offsetY2 = offsetY * 2; + Real diff = 0; + for (int j = 0; j < (sGridSize - 1); j += 2) { + HeightMapPixel* pMapRow = &pMap[(pixelY + j * stepY) * mMap->getWidth() + pixelX]; + for (int i = 0; i < (sGridSize - 1); i += 2) { + // dx + diff = maxf(diff, fabs((getOffsetPixel(0) + getOffsetPixel(offsetX2)) / 2.0f - getOffsetPixel(offsetX))); + diff = maxf(diff, fabs((getOffsetPixel(offsetY2) + getOffsetPixel(offsetY2 + offsetX2)) / 2.0f - getOffsetPixel(offsetY + offsetX))); + // dy + diff = maxf(diff, fabs((getOffsetPixel(0) + getOffsetPixel(offsetY2)) / 2.0f - getOffsetPixel(offsetY))); + diff = maxf(diff, fabs((getOffsetPixel(offsetX2) + getOffsetPixel(offsetX2 + offsetY2)) / 2.0f - getOffsetPixel(offsetX + offsetY))); + // diag + diff = maxf(diff, fabs((getOffsetPixel(offsetX2) + getOffsetPixel(offsetY2)) / 2.0f - getOffsetPixel(offsetY + offsetX))); + + pMapRow += offsetX2; + } + } + mLODDifference = diff / PlanetMapBuffer::LEVEL_RANGE; + + // Calculate LOD error of sphere. + Real angle = Math::PI / (sGridSize << maxi(0, mQuadTreeNode->mLOD - 1)); + Real sphereError = (1 - cos(angle)) * 1.4f * mPlanetRadius; + if (mPlanetHeight) { + mLODDifference += sphereError / mPlanetHeight; + // Convert to world units. + mDistance = mLODDifference * mPlanetHeight; + } + else { + mDistance = sphereError; + } + + // Cache square. + mDistanceSquared = mDistance * mDistance; + + srand(2134); + + //#define getPixel() ((((float)(((*(pMapRow)) & PlanetMapBuffer::LEVEL_MASK) >> PlanetMapBuffer::LEVEL_SHIFT)) - PlanetMapBuffer::LEVEL_MIN) / PlanetMapBuffer::LEVEL_RANGE) +#define getPixel() ((Bitwise::halfToFloat(*((unsigned short*)(pMapRow))) - PlanetMapBuffer::LEVEL_MIN) / PlanetMapBuffer::LEVEL_RANGE) + //#define getPixel() (((*((float*)(pMapRow))) - PlanetMapBuffer::LEVEL_MIN) / PlanetMapBuffer::LEVEL_RANGE) + + // (nVidia cards) cubemapping fills in the edge/corner texels of cubemaps with 0.5 pixel on both sides. + float uvCorrection = (PlanetMap::PLANET_TEXTURE_SIZE - 1.0f) / PlanetMap::PLANET_TEXTURE_SIZE; + + // Process vertex data for regular grid. + for (int j = 0; j < sGridSize; j++) { + HeightMapPixel* pMapRow = pMapCorner + j * offsetY; + for (int i = 0; i < sGridSize; i++) { + Real height = getPixel(); + Real x = (float) i / (float) (sGridSize - 1); + Real y = (float) j / (float) (sGridSize - 1); + + Vector3 spherePoint(x * invScale + positionX, y * invScale + positionY, 1); + spherePoint.normalise(); + spherePoint = faceTransform * spherePoint; + spherePoint *= mPlanetRadius + height * mPlanetHeight; + + mCenter += spherePoint; + + min.makeFloor(spherePoint); + max.makeCeil(spherePoint); + + pMapRow += offsetX; + } + } + + // Calculate center. + mSurfaceNormal = mCenter /= (sGridSize * sGridSize); + mSurfaceNormal.normalise(); + + // Set bounding box/radius. + setBoundingBox(AxisAlignedBox(min, max)); + mBoundingRadius = (max - min).length() / 2; + mBoxCenter = (max + min) / 2; +} + /** * Creates the vertex declaration. */ @@ -123,8 +262,8 @@ void PlanetRenderable::createVertexDeclaration() { sVertexDeclaration->addElement(0, offset, VET_FLOAT3, VES_POSITION); offset += VertexElement::getTypeSize(VET_FLOAT3); // Cube map coords - sVertexDeclaration->addElement(0, offset, VET_FLOAT3, VES_TEXTURE_COORDINATES, 0); - offset += VertexElement::getTypeSize(VET_FLOAT3); + sVertexDeclaration->addElement(0, offset, VET_FLOAT2, VES_TEXTURE_COORDINATES, 0); + offset += VertexElement::getTypeSize(VET_FLOAT2); } /** @@ -176,14 +315,13 @@ void PlanetRenderable::fillHardwareBuffers() { Real x = (float) i / (float) (sGridSize - 1); Real y = (float) j / (float) (sGridSize - 1); - *pTex++ = x; - *pTex++ = y; - *pTex++ = 1.0f; - *pPos++ = x; *pPos++ = y; *pPos++ = 0.0f; - + + *pTex++ = x; + *pTex++ = y; + pBase += sVertexBuffer->getVertexSize(); } } @@ -198,14 +336,13 @@ void PlanetRenderable::fillHardwareBuffers() { Real x = (float) i / (float) (sGridSize - 1); Real y = (float) j / (float) (sGridSize - 1); - *pTex++ = x; - *pTex++ = y; - *pTex++ = 1.0f; - *pPos++ = x; *pPos++ = y; *pPos++ = -1.0f; + *pTex++ = x; + *pTex++ = y; + pBase += sVertexBuffer->getVertexSize(); } } @@ -220,14 +357,13 @@ void PlanetRenderable::fillHardwareBuffers() { Real x = (float) i / (float) (sGridSize - 1); Real y = (float) j / (float) (sGridSize - 1); - *pTex++ = x; - *pTex++ = y; - *pTex++ = 1.0f; - *pPos++ = x; *pPos++ = y; *pPos++ = -1.0f; + *pTex++ = x; + *pTex++ = y; + pBase += sVertexBuffer->getVertexSize(); } } @@ -312,113 +448,6 @@ void PlanetRenderable::fillHardwareBuffers() { sVertexBuffer->unlock(); } -/** - * Analyse the terrain for this tile. - */ -void PlanetRenderable::analyseTerrain() { - // Examine pixel buffer to identify - HeightMapPixel* pMap = (HeightMapPixel*)(mMap->getData()); - - // Calculate scales, offsets and steps. - const Real scale = (Real)(1 << mQuadTreeNode->mLOD); - const Real invScale = 2.0f / scale; - const Real positionX = -1.f + invScale * mQuadTreeNode->mX; - const Real positionY = -1.f + invScale * mQuadTreeNode->mY; - const int stepX = (mMap->getWidth() - 1) / (1 << mQuadTreeNode->mLOD) / (sGridSize - 1); - const int stepY = (mMap->getHeight() - 1) / (1 << mQuadTreeNode->mLOD) / (sGridSize - 1); - const int pixelX = mQuadTreeNode->mX * stepX * (sGridSize - 1); - const int pixelY = mQuadTreeNode->mY * stepY * (sGridSize - 1); - const int offsetX = stepX; - const int offsetY = stepY * mMap->getWidth(); - - HeightMapPixel* pMapCorner = &pMap[pixelY * mMap->getWidth() + pixelX]; - - // Keep track of extents. - Vector3 min = Vector3(1e8), max = Vector3(-1e8); - mCenter = Vector3(0, 0, 0); - - Matrix3 faceTransform = PlanetCube::getFaceTransform(mQuadTreeNode->mFace, true); - - //#define getOffsetPixel(x) ((float)(((*(pMapRow + (x))) & PlanetMapBuffer::LEVEL_MASK) >> PlanetMapBuffer::LEVEL_SHIFT)) - #define getOffsetPixel(x) Bitwise::halfToFloat(*((unsigned short*)(pMapRow + (x)))) - //#define getOffsetPixel(x) (*((float*)(pMapRow + (x)))) - - // Lossy representation of heightmap - const int offsetX2 = offsetX * 2; - const int offsetY2 = offsetY * 2; - Real diff = 0; - for (int j = 0; j < (sGridSize - 1); j += 2) { - HeightMapPixel* pMapRow = &pMap[(pixelY + j * stepY) * mMap->getWidth() + pixelX]; - for (int i = 0; i < (sGridSize - 1); i += 2) { - // dx - diff = maxf(diff, fabs((getOffsetPixel(0) + getOffsetPixel(offsetX2)) / 2.0f - getOffsetPixel(offsetX))); - diff = maxf(diff, fabs((getOffsetPixel(offsetY2) + getOffsetPixel(offsetY2 + offsetX2)) / 2.0f - getOffsetPixel(offsetY + offsetX))); - // dy - diff = maxf(diff, fabs((getOffsetPixel(0) + getOffsetPixel(offsetY2)) / 2.0f - getOffsetPixel(offsetY))); - diff = maxf(diff, fabs((getOffsetPixel(offsetX2) + getOffsetPixel(offsetX2 + offsetY2)) / 2.0f - getOffsetPixel(offsetX + offsetY))); - // diag - diff = maxf(diff, fabs((getOffsetPixel(offsetX2) + getOffsetPixel(offsetY2)) / 2.0f - getOffsetPixel(offsetY + offsetX))); - - pMapRow += offsetX2; - } - } - mLODDifference = diff / PlanetMapBuffer::LEVEL_RANGE; - - // Calculate LOD error of sphere. - Real angle = Math::PI / (sGridSize << maxi(0, mQuadTreeNode->mLOD - 1)); - Real sphereError = (1 - cos(angle)) * 1.4f * mPlanetRadius; - if (mPlanetHeight) { - mLODDifference += sphereError / mPlanetHeight; - // Convert to world units. - mDistance = mLODDifference * mPlanetHeight; - } - else { - mDistance = sphereError; - } - - // Cache square. - mDistanceSquared = mDistance * mDistance; - - srand(2134); - - //#define getPixel() ((((float)(((*(pMapRow)) & PlanetMapBuffer::LEVEL_MASK) >> PlanetMapBuffer::LEVEL_SHIFT)) - PlanetMapBuffer::LEVEL_MIN) / PlanetMapBuffer::LEVEL_RANGE) - #define getPixel() ((Bitwise::halfToFloat(*((unsigned short*)(pMapRow))) - PlanetMapBuffer::LEVEL_MIN) / PlanetMapBuffer::LEVEL_RANGE) - //#define getPixel() (((*((float*)(pMapRow))) - PlanetMapBuffer::LEVEL_MIN) / PlanetMapBuffer::LEVEL_RANGE) - - // (nVidia cards) cubemapping fills in the edge/corner texels of cubemaps with 0.5 pixel on both sides. - float uvCorrection = (PlanetMap::PLANET_TEXTURE_SIZE - 1.0f) / PlanetMap::PLANET_TEXTURE_SIZE; - - // Process vertex data for regular grid. - for (int j = 0; j < sGridSize; j++) { - HeightMapPixel* pMapRow = pMapCorner + j * offsetY; - for (int i = 0; i < sGridSize; i++) { - Real height = getPixel(); - Real x = (float) i / (float) (sGridSize - 1); - Real y = (float) j / (float) (sGridSize - 1); - - Vector3 spherePoint(x * invScale + positionX, y * invScale + positionY, 1); - spherePoint.normalise(); - spherePoint = faceTransform * spherePoint; - spherePoint *= mPlanetRadius + height * mPlanetHeight; - - mCenter += spherePoint; - - min.makeFloor(spherePoint); - max.makeCeil(spherePoint); - - pMapRow += offsetX; - } - } - - // Calculate center. - mSurfaceNormal = mCenter /= (sGridSize * sGridSize); - mSurfaceNormal.normalise(); - - // Set bounding box/radius. - setBoundingBox(AxisAlignedBox(min, max)); - mBoundingRadius = maxf((max - mCenter).length() / 2, (min - mCenter).length() / 2); -} - bool PlanetRenderable::preRender(SceneManager* sm, RenderSystem* rsys) { return true; } @@ -426,8 +455,20 @@ bool PlanetRenderable::preRender(SceneManager* sm, RenderSystem* rsys) { void PlanetRenderable::postRender(SceneManager* sm, RenderSystem* rsys) { } +void PlanetRenderable::updateRenderQueue(RenderQueue* queue) { + _updateRenderQueue(queue); +} + void PlanetRenderable::_updateRenderQueue(RenderQueue* queue) { SimpleRenderable::_updateRenderQueue(queue); + + /* + if (mWireBoundingBox == NULL) { + mWireBoundingBox = OGRE_NEW WireBoundingBox(); + } + mWireBoundingBox->setupBoundingBox(mBox); + queue->addRenderable(mWireBoundingBox); + */ } const String& PlanetRenderable::getMovableType(void) const { @@ -437,8 +478,8 @@ const String& PlanetRenderable::getMovableType(void) const { void PlanetRenderable::setFrameOfReference(SimpleFrustum& frustum, Vector3 cameraPosition, Vector3 cameraPlane, Real sphereClip, Real lodDetailFactorSquared) { // Bounding box clipping. - Sphere boundingSphere = Sphere(mCenter, mBoundingRadius); - if (!frustum.isVisible(&boundingSphere)) { + //Sphere boundingSphere = Sphere(mBoxCenter, mBoundingRadius); + if (!frustum.isVisible(mBox)) { mIsClipped = true; return; } @@ -489,10 +530,7 @@ Real PlanetRenderable::getBoundingRadius(void) const Real PlanetRenderable::getSquaredViewDepth(const Camera* cam) const { Vector3 vMin, vMax, vMid, vDist; - vMin = mBox.getMinimum(); - vMax = mBox.getMaximum(); - vMid = ((vMax - vMin) * 0.5) + vMin; - vDist = cam->getDerivedPosition() - vMid; + vDist = cam->getDerivedPosition() - mCenter; return vDist.squaredLength(); } diff --git a/Source/Planet/Mesh/PlanetRenderable.h b/Source/Planet/Mesh/PlanetRenderable.h index d2536ea..2bc5df3 100644 --- a/Source/Planet/Mesh/PlanetRenderable.h +++ b/Source/Planet/Mesh/PlanetRenderable.h @@ -16,6 +16,7 @@ namespace NFSpace { }; #include "Ogre/OgreSimpleRenderable.h" +#include "Ogre/OgreWireBoundingBox.h" #include "SimpleFrustum.h" #include "PlanetCube.h" @@ -47,6 +48,9 @@ class PlanetRenderable : public SimpleRenderable { const bool isInLODRange() const; const bool isClipped() const; + virtual void updateRenderQueue(RenderQueue* queue); + Vector3 mSurfaceNormal; + protected: static int sInstances; static VertexData* sVertexData; @@ -63,7 +67,7 @@ class PlanetRenderable : public SimpleRenderable { Real mBoundingRadius; Vector3 mCenter; - Vector3 mSurfaceNormal; + Vector3 mBoxCenter; Real mChildDistance; Real mChildDistanceSquared; @@ -81,6 +85,7 @@ class PlanetRenderable : public SimpleRenderable { bool mIsInLODRange; bool mIsClipped; + WireBoundingBox* mWireBoundingBox; const QuadTreeNode* mQuadTreeNode; /** @@ -102,6 +107,7 @@ class PlanetRenderable : public SimpleRenderable { Real PlanetRenderable::getBoundingRadius(void) const; Real getSquaredViewDepth(const Camera* cam) const; + void initDisplacementMapping(); virtual void analyseTerrain(); }; diff --git a/Source/Planet/PlanetMovable.cpp b/Source/Planet/PlanetMovable.cpp index 7a34121..3b69f68 100644 --- a/Source/Planet/PlanetMovable.cpp +++ b/Source/Planet/PlanetMovable.cpp @@ -8,39 +8,44 @@ */ #include "PlanetMovable.h" +#include "EngineState.h" namespace NFSpace { String PlanetMovable::MOVABLE_TYPE_NAME = "PlanetMovable"; -PlanetMovable::PlanetMovable() -: MovableObject() { +PlanetMovable::PlanetMovable(PlanetDescriptor descriptor) +: mDescriptor(descriptor), MovableObject() { initObject(); } -PlanetMovable::PlanetMovable(const String& name) -: MovableObject(name) { +PlanetMovable::PlanetMovable(PlanetDescriptor descriptor, const String& name) +: mDescriptor(descriptor), MovableObject(name) { initObject(); } PlanetMovable::~PlanetMovable() { - delete mCube; - delete mMap; + deleteObject(); } -void PlanetMovable::refresh() { - delete mCube; - delete mMap; +void PlanetMovable::refresh(PlanetDescriptor descriptor) { + deleteObject(); + mDescriptor = descriptor; initObject(); } void PlanetMovable::initObject() { setListener(this); - mMap = new PlanetMap(); + mMap = new PlanetMap(&mDescriptor); mCube = new PlanetCube(this, mMap); mBoundingBox = AxisAlignedBox(Vector3(-mCube->getScale()), Vector3(mCube->getScale())); } +void PlanetMovable::deleteObject() { + delete mCube; + delete mMap; +} + void PlanetMovable::_notifyCurrentCamera(Camera* camera) { mCube->setCamera(camera); } @@ -101,12 +106,24 @@ bool PlanetMovable::objectRendering(const MovableObject* movableObject, const Ca String PlanetMovableFactory::FACTORY_TYPE_NAME = "PlanetMovable"; +PlanetDescriptor PlanetMovableFactory::getDefaultDescriptor() { + PlanetDescriptor descriptor; + + descriptor.seed = getInt("planet.seed"); + descriptor.brushes = getInt("planet.brushes"); + descriptor.radius = getReal("planet.radius"); + descriptor.height = getReal("planet.height"); + descriptor.lodLimit = getInt("planet.lodLimit"); + + return descriptor; +} + const String& PlanetMovableFactory::getType(void) const { return FACTORY_TYPE_NAME; } MovableObject* PlanetMovableFactory::createInstanceImpl(const String& name, const NameValuePairList* params) { - return new PlanetMovable(name); + return new PlanetMovable(getDefaultDescriptor(), name); } void PlanetMovableFactory::destroyInstance(MovableObject* obj) { diff --git a/Source/Planet/PlanetMovable.h b/Source/Planet/PlanetMovable.h index 6582882..a34a068 100644 --- a/Source/Planet/PlanetMovable.h +++ b/Source/Planet/PlanetMovable.h @@ -15,6 +15,7 @@ #include #include +#include "PlanetDescriptor.h" #include "PlanetCube.h" #include "PlanetMap.h" @@ -31,15 +32,16 @@ namespace NFSpace { */ class PlanetMovable : public MovableObject, public MovableObject::Listener { public: - PlanetMovable(); - PlanetMovable(const String& name); + PlanetMovable(PlanetDescriptor descriptor); + PlanetMovable(PlanetDescriptor descriptor, const String& name); virtual ~PlanetMovable(); + PlanetCube* mCube; PlanetMap* mMap; protected: static String MOVABLE_TYPE_NAME; - PlanetCube* mCube; + PlanetDescriptor mDescriptor; AxisAlignedBox mBoundingBox; bool mInited; @@ -49,7 +51,12 @@ class PlanetMovable : public MovableObject, public MovableObject::Listener { */ void initObject(); - void refresh(); + /** + * Clean up the planet object. + */ + void deleteObject(); + + void refresh(PlanetDescriptor descriptor); /** * Callback: MovableObject has been destroyed. @@ -93,6 +100,8 @@ class PlanetMovableFactory : public MovableObjectFactory { ~PlanetMovableFactory() {} static String FACTORY_TYPE_NAME; + + static PlanetDescriptor getDefaultDescriptor(); const String& getType(void) const; void destroyInstance(MovableObject* obj);