载入中...
搜索中...
未找到
VirtualTextureBlend.cpp
浏览该文件的文档.
2
3#include "image/ImageData.h"
4
5#include <algorithm>
6#include <cmath>
7#include <functional>
8#include <unordered_set>
9
10namespace eve::graphics {
11namespace {
12
14
15eve::Diagnostic invalid(std::string message) {
17}
18
19float wrap01(float value) {
20 value -= std::floor(value);
21 return value < 0.f ? value + 1.f : value;
22}
23
24Color sampleRepeat(const image::ImageData& image, float u, float v) {
25 const int width = image.getWidth();
26 const int height = image.getHeight();
27 const float x = wrap01(u) * float(width) - 0.5f;
28 const float y = wrap01(v) * float(height) - 0.5f;
29 const int x0 = int(std::floor(x));
30 const int y0 = int(std::floor(y));
31 const float tx = x - float(x0);
32 const float ty = y - float(y0);
33 auto pixel = [&](int px, int py) {
34 px = (px % width + width) % width;
35 py = (py % height + height) % height;
36 return image.getPixel(px, py);
37 };
38 const Color a = pixel(x0, y0);
39 const Color b = pixel(x0 + 1, y0);
40 const Color c = pixel(x0, y0 + 1);
41 const Color d = pixel(x0 + 1, y0 + 1);
42 const auto mix = [tx, ty](float av, float bv, float cv, float dv) {
43 return (av + (bv - av) * tx) + ((cv + (dv - cv) * tx) - (av + (bv - av) * tx)) * ty;
44 };
45 Color out{mix(a.r, b.r, c.r, d.r), mix(a.g, b.g, c.g, d.g), mix(a.b, b.b, c.b, d.b), mix(a.a, b.a, c.a, d.a)};
46 return out;
47}
48
49std::shared_ptr<const image::ImageData> copyImage(const image::ImageData& source) {
50 return std::make_shared<image::ImageData>(source);
51}
52
53} // namespace
54
56 if (config.width <= 0 || config.height <= 0)
58 invalid("virtual texture dimensions must be positive"));
59 if (config.tileSize < 4 || config.tileSize > 1024)
61 invalid("virtual texture tile size must be in [4, 1024]"));
64 invalid("virtual texture border must be non-negative and smaller than half a tile"));
67 invalid("virtual texture resident page capacity must be positive"));
69 std::unique_ptr<VirtualTextureBlend>(new VirtualTextureBlend(config)));
70}
71
73 if (!layer.albedo) return eve::Result<std::uint32_t>::failure(invalid("virtual texture layer requires albedo"));
74 if (layer.albedo->getWidth() <= 0 || layer.albedo->getHeight() <= 0 || layer.albedo->getFormat() != "RGBA8")
75 return eve::Result<std::uint32_t>::failure(invalid("layer albedo must be non-empty RGBA8"));
76 if (layer.normal && layer.normal->getFormat() != "RGBA8")
77 return eve::Result<std::uint32_t>::failure(invalid("layer normal must be RGBA8"));
78 if (layer.weight && layer.weight->getFormat() != "RGBA8")
79 return eve::Result<std::uint32_t>::failure(invalid("layer weight must be RGBA8"));
80 if (!std::isfinite(layer.uvScale) || layer.uvScale <= 0.f || !std::isfinite(layer.constantWeight) ||
81 layer.constantWeight < 0.f || !std::isfinite(layer.heightContrast) || layer.heightContrast < 0.f)
82 return eve::Result<std::uint32_t>::failure(invalid("layer blend parameters are invalid"));
83 if (layers_.size() >= 16)
84 return eve::Result<std::uint32_t>::failure(invalid("virtual texture supports at most 16 layers"));
85
87 owned.albedo = copyImage(*layer.albedo);
88 if (layer.normal) owned.normal = copyImage(*layer.normal);
89 if (layer.weight) owned.weight = copyImage(*layer.weight);
90 layers_.push_back(std::move(owned));
91 invalidate();
92 return eve::Result<std::uint32_t>::success(std::uint32_t(layers_.size() - 1));
93}
94
95std::size_t VirtualTextureBlend::PageKeyHash::operator()(const PageKey& key) const noexcept {
96 std::size_t seed = std::hash<int>{}(key.x);
97 seed ^= std::hash<int>{}(key.y) + 0x9e3779b9u + (seed << 6u) + (seed >> 2u);
98 seed ^= std::hash<int>{}(key.mip) + 0x9e3779b9u + (seed << 6u) + (seed >> 2u);
99 seed ^= std::hash<int>{}(int(key.attribute)) + 0x9e3779b9u + (seed << 6u) + (seed >> 2u);
100 return seed;
101}
102
103int VirtualTextureBlend::pageCountX(int mipLevel) const {
104 if (mipLevel < 0 || mipLevel > 30) return 0;
105 const int width = std::max(1, config_.width >> mipLevel);
106 return (width + config_.tileSize - 1) / config_.tileSize;
107}
108
109int VirtualTextureBlend::pageCountY(int mipLevel) const {
110 if (mipLevel < 0 || mipLevel > 30) return 0;
111 const int height = std::max(1, config_.height >> mipLevel);
112 return (height + config_.tileSize - 1) / config_.tileSize;
113}
114
116 int mipLevel) {
117 return requestPage({pageX, pageY, mipLevel, Attribute::Albedo});
118}
119
121 int mipLevel) {
122 return requestPage({pageX, pageY, mipLevel, Attribute::Normal});
123}
124
125eve::Result<std::shared_ptr<const image::ImageData>> VirtualTextureBlend::requestPage(PageKey key) {
126 if (layers_.empty())
128 invalid("virtual texture has no material layers"));
129 if (key.mip < 0 || key.mip > 30 || key.x < 0 || key.y < 0 || key.x >= pageCountX(key.mip) ||
130 key.y >= pageCountY(key.mip))
132 invalid("virtual texture page coordinate is outside the mip"));
133 auto found = cache_.find(key);
134 if (found != cache_.end()) {
135 ++hits_;
136 lru_.splice(lru_.begin(), lru_, found->second.lru);
137 return eve::Result<std::shared_ptr<const image::ImageData>>::success(found->second.image);
138 }
139
140 ++misses_;
141 auto page = composePage(key);
142 if (cache_.size() == config_.residentPageCapacity) {
143 cache_.erase(lru_.back());
144 lru_.pop_back();
145 ++evictions_;
146 }
147 lru_.push_front(key);
148 cache_.emplace(key, CacheEntry{page, lru_.begin()});
149 return eve::Result<std::shared_ptr<const image::ImageData>>::success(std::move(page));
150}
151
152std::shared_ptr<const image::ImageData> VirtualTextureBlend::composePage(const PageKey& key) const {
153 const int extent = config_.tileSize + config_.borderSize * 2;
154 auto page = std::make_shared<image::ImageData>(extent, extent, "RGBA8");
155 const int mipWidth = std::max(1, config_.width >> key.mip);
156 const int mipHeight = std::max(1, config_.height >> key.mip);
157 std::vector<float> influences(layers_.size());
158 std::vector<Color> samples(layers_.size());
159 for (int py = 0; py < extent; ++py) {
160 for (int px = 0; px < extent; ++px) {
161 const int virtualX = key.x * config_.tileSize + px - config_.borderSize;
162 const int virtualY = key.y * config_.tileSize + py - config_.borderSize;
163 const float u = (float(virtualX) + 0.5f) / float(mipWidth);
164 const float v = (float(virtualY) + 0.5f) / float(mipHeight);
165 float maximum = 0.f;
166 for (std::size_t i = 0; i < layers_.size(); ++i) {
167 const auto& layer = layers_[i];
168 const Color albedo = sampleRepeat(*layer.albedo, u * layer.uvScale, v * layer.uvScale);
169 const float mask = layer.weight ? sampleRepeat(*layer.weight, u, v).r : 1.f;
170 influences[i] = layer.constantWeight * mask + albedo.a * layer.heightContrast;
171 maximum = std::max(maximum, influences[i]);
172 if (key.attribute == Attribute::Normal && layer.normal)
173 samples[i] = sampleRepeat(*layer.normal, u * layer.uvScale, v * layer.uvScale);
174 else if (key.attribute == Attribute::Normal)
175 samples[i] = Color{0.5f, 0.5f, 1.f, 1.f};
176 else
177 samples[i] = albedo;
178 }
179 float total = 0.f;
180 constexpr float transition = 0.15f;
181 for (float& influence : influences) {
182 influence = std::max(0.f, influence - maximum + transition);
183 total += influence;
184 }
185 if (total <= 1e-6f) {
186 influences.front() = 1.f;
187 total = 1.f;
188 }
189 Color output{0.f, 0.f, 0.f, 0.f};
190 for (std::size_t i = 0; i < samples.size(); ++i) {
191 const float weight = influences[i] / total;
192 if (key.attribute == Attribute::Normal) {
193 output.r += (samples[i].r * 2.f - 1.f) * weight;
194 output.g += (samples[i].g * 2.f - 1.f) * weight;
195 output.b += (samples[i].b * 2.f - 1.f) * weight;
196 } else {
197 output.r += samples[i].r * weight;
198 output.g += samples[i].g * weight;
199 output.b += samples[i].b * weight;
200 output.a += samples[i].a * weight;
201 }
202 }
203 if (key.attribute == Attribute::Normal) {
204 const float length = std::sqrt(output.r * output.r + output.g * output.g + output.b * output.b);
205 const float inverse = length > 1e-6f ? 1.f / length : 1.f;
206 output = Color{output.r * inverse * 0.5f + 0.5f, output.g * inverse * 0.5f + 0.5f,
207 output.b * inverse * 0.5f + 0.5f, 1.f};
208 }
209 page->setPixel(px, py, output);
210 }
211 }
212 return page;
213}
214
216 return bakeAttribute(Attribute::Albedo);
217}
218
220 return bakeAttribute(Attribute::Normal);
221}
222
223eve::Result<std::unique_ptr<image::ImageData>> VirtualTextureBlend::bakeAttribute(Attribute attribute) {
224 if (layers_.empty())
226 invalid("virtual texture has no material layers"));
227 auto output = std::make_unique<image::ImageData>(config_.width, config_.height, "RGBA8");
228 for (int pageY = 0; pageY < pageCountY(); ++pageY) {
229 for (int pageX = 0; pageX < pageCountX(); ++pageX) {
230 auto pageResult = requestPage({pageX, pageY, 0, attribute});
231 if (!pageResult.ok()) return eve::Result<std::unique_ptr<image::ImageData>>::failure(pageResult.status());
232 const auto& page = *pageResult.value();
233 const int copyWidth = std::min(config_.tileSize, config_.width - pageX * config_.tileSize);
234 const int copyHeight = std::min(config_.tileSize, config_.height - pageY * config_.tileSize);
235 output->paste(const_cast<image::ImageData*>(&page), pageX * config_.tileSize, pageY * config_.tileSize,
236 config_.borderSize, config_.borderSize, copyWidth, copyHeight);
237 }
238 }
239 return eve::Result<std::unique_ptr<image::ImageData>>::success(std::move(output));
240}
241
243 const std::vector<VirtualTexturePageId>& residentPages) {
244 if (layers_.empty())
245 return eve::Result<VirtualTextureResidentSet>::failure(invalid("virtual texture has no material layers"));
246 if (residentPages.size() > config_.residentPageCapacity)
248 invalid("resident page selection exceeds physical cache capacity"));
249
250 std::unordered_set<std::uint64_t> unique;
251 for (const auto& page : residentPages) {
252 if (page.x < 0 || page.y < 0 || page.x >= pageCountX() || page.y >= pageCountY())
254 invalid("resident page coordinate is outside mip zero"));
255 const auto key = (std::uint64_t(std::uint32_t(page.y)) << 32u) | std::uint32_t(page.x);
256 if (!unique.insert(key).second)
258 invalid("resident page selection contains duplicates"));
259 }
260
261 const int extent = config_.tileSize + config_.borderSize * 2;
262 const std::size_t slotCount = config_.residentPageCapacity + 1;
263 const int slotsX = int(std::ceil(std::sqrt(double(slotCount))));
264 const int slotsY = int((slotCount + std::size_t(slotsX) - 1) / std::size_t(slotsX));
266 output.albedoAtlas = std::make_unique<image::ImageData>(slotsX * extent, slotsY * extent, "RGBA8");
267 output.normalAtlas = std::make_unique<image::ImageData>(slotsX * extent, slotsY * extent, "RGBA8");
268 output.pageTable = std::make_unique<image::ImageData>(pageCountX(), pageCountY(), "RGBA8");
269 output.atlasSlotsX = slotsX;
270 output.atlasSlotsY = slotsY;
271 output.pageExtent = extent;
272 output.borderFraction = float(config_.borderSize) / float(extent);
273 output.residentPages = residentPages.size();
274
275 auto albedoFallback = bakeAlbedo();
276 if (!albedoFallback.ok()) return eve::Result<VirtualTextureResidentSet>::failure(albedoFallback.status());
277 auto normalFallback = bakeNormal();
278 if (!normalFallback.ok()) return eve::Result<VirtualTextureResidentSet>::failure(normalFallback.status());
279
280 auto writeFallback = [&](image::ImageData& atlas, const image::ImageData& source) {
281 for (int y = 0; y < extent; ++y) {
282 for (int x = 0; x < extent; ++x) {
283 const float u = (float(x - config_.borderSize) + 0.5f) / float(config_.tileSize);
284 const float v = (float(y - config_.borderSize) + 0.5f) / float(config_.tileSize);
285 atlas.setPixel(x, y, sampleRepeat(source, u, v));
286 }
287 }
288 };
289 writeFallback(*output.albedoAtlas, *albedoFallback.value());
290 writeFallback(*output.normalAtlas, *normalFallback.value());
291
292 const Color fallbackEntry{0.5f / float(slotsX), 0.5f / float(slotsY), 0.f, 1.f};
293 for (int y = 0; y < pageCountY(); ++y)
294 for (int x = 0; x < pageCountX(); ++x) output.pageTable->setPixel(x, y, fallbackEntry);
295
296 for (std::size_t index = 0; index < residentPages.size(); ++index) {
297 const int slot = int(index) + 1;
298 const int slotX = slot % slotsX;
299 const int slotY = slot / slotsX;
300 auto albedo = requestAlbedoPage(residentPages[index].x, residentPages[index].y, 0);
302 auto normal = requestNormalPage(residentPages[index].x, residentPages[index].y, 0);
304 output.albedoAtlas->paste(const_cast<image::ImageData*>(albedo.value().get()), slotX * extent, slotY * extent,
305 0, 0, extent, extent);
306 output.normalAtlas->paste(const_cast<image::ImageData*>(normal.value().get()), slotX * extent, slotY * extent,
307 0, 0, extent, extent);
308 const Color entry{(float(slotX) + 0.5f) / float(slotsX), (float(slotY) + 0.5f) / float(slotsY), 1.f, 1.f};
309 output.pageTable->setPixel(residentPages[index].x, residentPages[index].y, entry);
310 }
312}
313
315 cache_.clear();
316 lru_.clear();
317}
318
319VirtualTextureBlendStats VirtualTextureBlend::stats() const { return {cache_.size(), hits_, misses_, evictions_}; }
320
321} // namespace eve::graphics
double value
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
std::string output
ActiveSource owned
int mask
float length
Definition CaveMesh.cpp:94
float py
graphics::Texture * albedo
std::string message
float maximum[3]
std::uint32_t key
vk::UniqueImage image
float u
Definition Grass.cpp:233
float v
std::int32_t c
std::vector< Colorf > px
std::uint32_t height
std::uint32_t width
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
Texture * normal
TileLayer * layer
std::uint32_t seed
Definition PointSet.cpp:807
float d
bool found
uint32_t index
const UnitySourceAsset & source
A structured explanation of a failed, degraded, or noteworthy result.
Definition Diagnostic.h:94
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
CPU reference implementation of a tiled, height-aware material virtual texture.
eve::Result< std::unique_ptr< image::ImageData > > bakeNormal()
Bake the complete mip-zero tangent-space normal substitute for nonresident pages.
eve::Result< std::uint32_t > addLayer(const VirtualTextureBlendLayer &layer)
Copy and append one material layer, invalidating generated pages atomically.
static eve::Result< std::unique_ptr< VirtualTextureBlend > > create(const VirtualTextureBlendConfig &config)
Validate configuration and create an empty virtual texture.
eve::Result< std::shared_ptr< const image::ImageData > > requestAlbedoPage(int pageX, int pageY, int mipLevel)
Resolve one page at the requested mip level.
int pageCountX(int mipLevel=0) const
Page count x.
eve::Result< std::unique_ptr< image::ImageData > > bakeAlbedo()
Bake the complete mip-zero albedo substitute used for nonresident pages.
eve::Result< std::shared_ptr< const image::ImageData > > requestNormalPage(int pageX, int pageY, int mipLevel)
Resolve a tangent-space normal page using vector blending and renormalization.
eve::Result< VirtualTextureResidentSet > buildResidentSet(const std::vector< VirtualTexturePageId > &residentPages)
Build an atlas, page table, and whole-surface substitute for selected mip-zero pages.
void invalidate()
Clear generated pages while preserving immutable source layers.
const VirtualTextureBlendConfig & config() const
Config.
int pageCountY(int mipLevel=0) const
Page count y.
VirtualTextureBlendStats stats() const
Snapshot current cache counters.
Represents raw pixel data.
Definition ImageData.h:38
void setPixel(int x, int y, const Colorf &p)
Sets the pixel at location (x,y).
medialoader::Colorf Colorf
Definition ImageData.h:41
卡牌游戏 UI 工具模块:工厂 + 脚本绑定入口。 功能参考 ycarowr/UiCard:扇形手牌布局、抽牌/洗牌、悬浮放大、拖拽到落牌区、 敌方手牌(背面/偷看)、费用不足置灰,以及可实时调节的布局...
Definition Animation.h:25
eve::Color Color
RGBA color used by every graphics draw call. Lives inside eve::graphics so including a graphics heade...
Definition Color.h:13
Result< int > invalid(std::string message)
Invalid.
Backend-neutral configuration for a paged material-attribute texture.
One immutable source layer copied into a VirtualTextureBlend.
std::shared_ptr< const image::ImageData > albedo
Observable residency counters for virtual-texture diagnostics.
CPU images and sampling metadata for one atomically publishable physical cache.
std::unique_ptr< image::ImageData > albedoAtlas