@littlecarlito/blorktools 0.50.3 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +69 -0
- package/package.json +13 -7
- package/src/asset_debugger/axis-indicator/axis-indicator.css +6 -0
- package/src/asset_debugger/axis-indicator/axis-indicator.html +20 -0
- package/src/asset_debugger/axis-indicator/axis-indicator.js +822 -0
- package/src/asset_debugger/debugger-scene/debugger-scene.css +142 -0
- package/src/asset_debugger/debugger-scene/debugger-scene.html +80 -0
- package/src/asset_debugger/debugger-scene/debugger-scene.js +791 -0
- package/src/asset_debugger/header/header.css +73 -0
- package/src/asset_debugger/header/header.html +24 -0
- package/src/asset_debugger/header/header.js +224 -0
- package/src/asset_debugger/index.html +76 -0
- package/src/asset_debugger/landing-page/landing-page.css +396 -0
- package/src/asset_debugger/landing-page/landing-page.html +81 -0
- package/src/asset_debugger/landing-page/landing-page.js +611 -0
- package/src/asset_debugger/loading-splash/loading-splash.css +195 -0
- package/src/asset_debugger/loading-splash/loading-splash.html +22 -0
- package/src/asset_debugger/loading-splash/loading-splash.js +59 -0
- package/src/asset_debugger/loading-splash/preview-loading-splash.js +66 -0
- package/src/asset_debugger/main.css +14 -0
- package/src/asset_debugger/modals/examples-modal/examples-modal.css +41 -0
- package/src/asset_debugger/modals/examples-modal/examples-modal.html +18 -0
- package/src/asset_debugger/modals/examples-modal/examples-modal.js +111 -0
- package/src/asset_debugger/modals/examples-modal/examples.js +125 -0
- package/src/asset_debugger/modals/html-editor-modal/html-editor-modal.css +452 -0
- package/src/asset_debugger/modals/html-editor-modal/html-editor-modal.html +87 -0
- package/src/asset_debugger/modals/html-editor-modal/html-editor-modal.js +675 -0
- package/src/asset_debugger/modals/mesh-info-modal/mesh-info-modal.css +219 -0
- package/src/asset_debugger/modals/mesh-info-modal/mesh-info-modal.html +20 -0
- package/src/asset_debugger/modals/mesh-info-modal/mesh-info-modal.js +548 -0
- package/src/asset_debugger/modals/settings-modal/settings-modal.css +103 -0
- package/src/asset_debugger/modals/settings-modal/settings-modal.html +158 -0
- package/src/asset_debugger/modals/settings-modal/settings-modal.js +475 -0
- package/src/asset_debugger/panels/asset-panel/asset-panel.css +263 -0
- package/src/asset_debugger/panels/asset-panel/asset-panel.html +123 -0
- package/src/asset_debugger/panels/asset-panel/asset-panel.js +136 -0
- package/src/asset_debugger/panels/asset-panel/atlas-heading/atlas-heading.css +94 -0
- package/src/asset_debugger/panels/asset-panel/atlas-heading/atlas-heading.js +312 -0
- package/src/asset_debugger/panels/asset-panel/mesh-heading/mesh-heading.css +129 -0
- package/src/asset_debugger/panels/asset-panel/mesh-heading/mesh-heading.js +486 -0
- package/src/asset_debugger/panels/asset-panel/rig-heading/rig-heading.css +545 -0
- package/src/asset_debugger/panels/asset-panel/rig-heading/rig-heading.js +538 -0
- package/src/asset_debugger/panels/asset-panel/uv-heading/uv-heading.css +70 -0
- package/src/asset_debugger/panels/asset-panel/uv-heading/uv-heading.js +586 -0
- package/src/asset_debugger/panels/world-panel/world-panel.css +364 -0
- package/src/asset_debugger/panels/world-panel/world-panel.html +173 -0
- package/src/asset_debugger/panels/world-panel/world-panel.js +1891 -0
- package/src/asset_debugger/router.js +190 -0
- package/src/asset_debugger/util/animation/playback/animation-playback-controller.js +150 -0
- package/src/asset_debugger/util/animation/playback/animation-preview-controller.js +316 -0
- package/src/asset_debugger/util/animation/playback/css3d-bounce-controller.js +400 -0
- package/src/asset_debugger/util/animation/playback/css3d-reversal-controller.js +821 -0
- package/src/asset_debugger/util/animation/render/css3d-prerender-controller.js +696 -0
- package/src/asset_debugger/util/animation/render/debug-texture-factory.js +0 -0
- package/src/asset_debugger/util/animation/render/iframe2texture-render-controller.js +199 -0
- package/src/asset_debugger/util/animation/render/image2texture-prerender-controller.js +461 -0
- package/src/asset_debugger/util/animation/render/pbr-material-factory.js +82 -0
- package/src/asset_debugger/util/common.css +280 -0
- package/src/asset_debugger/util/data/animation-classifier.js +323 -0
- package/src/asset_debugger/util/data/duplicate-handler.js +20 -0
- package/src/asset_debugger/util/data/glb-buffer-manager.js +407 -0
- package/src/asset_debugger/util/data/glb-classifier.js +290 -0
- package/src/asset_debugger/util/data/html-formatter.js +76 -0
- package/src/asset_debugger/util/data/html-linter.js +276 -0
- package/src/asset_debugger/util/data/localstorage-manager.js +265 -0
- package/src/asset_debugger/util/data/mesh-html-manager.js +295 -0
- package/src/asset_debugger/util/data/string-serder.js +303 -0
- package/src/asset_debugger/util/data/texture-classifier.js +663 -0
- package/src/asset_debugger/util/data/upload/background-file-handler.js +292 -0
- package/src/asset_debugger/util/data/upload/dropzone-preview-controller.js +396 -0
- package/src/asset_debugger/util/data/upload/file-upload-manager.js +495 -0
- package/src/asset_debugger/util/data/upload/glb-file-handler.js +36 -0
- package/src/asset_debugger/util/data/upload/glb-preview-controller.js +317 -0
- package/src/asset_debugger/util/data/upload/lighting-file-handler.js +194 -0
- package/src/asset_debugger/util/data/upload/model-file-manager.js +104 -0
- package/src/asset_debugger/util/data/upload/texture-file-handler.js +166 -0
- package/src/asset_debugger/util/data/upload/zip-handler.js +686 -0
- package/src/asset_debugger/util/loaders/html2canvas-loader.js +107 -0
- package/src/asset_debugger/util/rig/bone-kinematics.js +403 -0
- package/src/asset_debugger/util/rig/rig-constraint-manager.js +618 -0
- package/src/asset_debugger/util/rig/rig-controller.js +612 -0
- package/src/asset_debugger/util/rig/rig-factory.js +628 -0
- package/src/asset_debugger/util/rig/rig-handle-factory.js +46 -0
- package/src/asset_debugger/util/rig/rig-label-factory.js +441 -0
- package/src/asset_debugger/util/rig/rig-mouse-handler.js +377 -0
- package/src/asset_debugger/util/rig/rig-state-manager.js +175 -0
- package/src/asset_debugger/util/rig/rig-tooltip-manager.js +267 -0
- package/src/asset_debugger/util/rig/rig-ui-factory.js +700 -0
- package/src/asset_debugger/util/scene/background-manager.js +284 -0
- package/src/asset_debugger/util/scene/camera-controller.js +243 -0
- package/src/asset_debugger/util/scene/css3d-debug-controller.js +406 -0
- package/src/asset_debugger/util/scene/css3d-frame-factory.js +113 -0
- package/src/asset_debugger/util/scene/css3d-scene-manager.js +529 -0
- package/src/asset_debugger/util/scene/glb-controller.js +208 -0
- package/src/asset_debugger/util/scene/lighting-manager.js +690 -0
- package/src/asset_debugger/util/scene/threejs-model-manager.js +437 -0
- package/src/asset_debugger/util/scene/threejs-preview-manager.js +207 -0
- package/src/asset_debugger/util/scene/threejs-preview-setup.js +478 -0
- package/src/asset_debugger/util/scene/threejs-scene-controller.js +286 -0
- package/src/asset_debugger/util/scene/ui-manager.js +107 -0
- package/src/asset_debugger/util/state/animation-state.js +128 -0
- package/src/asset_debugger/util/state/css3d-state.js +83 -0
- package/src/asset_debugger/util/state/glb-preview-state.js +31 -0
- package/src/asset_debugger/util/state/log-util.js +197 -0
- package/src/asset_debugger/util/state/scene-state.js +452 -0
- package/src/asset_debugger/util/state/threejs-state.js +54 -0
- package/src/asset_debugger/util/workers/lighting-worker.js +61 -0
- package/src/asset_debugger/util/workers/model-worker.js +109 -0
- package/src/asset_debugger/util/workers/texture-worker.js +54 -0
- package/src/asset_debugger/util/workers/worker-manager.js +212 -0
- package/src/asset_debugger/widgets/mesh-info-widget.js +280 -0
- package/src/index.html +261 -0
- package/src/index.js +8 -0
- package/vite.config.js +66 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atlas Texture Type Classifier
|
|
3
|
+
*
|
|
4
|
+
* Identifies texture map types (base color, normal maps, ORM maps) from atlas files
|
|
5
|
+
* with high accuracy and configurable confidence thresholds.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Confidence level enum for texture classification
|
|
9
|
+
export const ConfidenceLevel = {
|
|
10
|
+
HIGH: "high",
|
|
11
|
+
MEDIUM: "medium",
|
|
12
|
+
LOW: "low",
|
|
13
|
+
UNKNOWN: "unknown"
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Global constant for the acceptable confidence level
|
|
17
|
+
// This determines what's considered a valid match during classification
|
|
18
|
+
let ACCEPTABLE_CONFIDENCE = ConfidenceLevel.MEDIUM;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Texture classifier for identifying atlas texture types
|
|
22
|
+
*/
|
|
23
|
+
export class TextureClassifier {
|
|
24
|
+
constructor() {
|
|
25
|
+
// Classification confidence thresholds
|
|
26
|
+
this.HIGH_CONFIDENCE = 0.8;
|
|
27
|
+
this.MEDIUM_CONFIDENCE = 0.6;
|
|
28
|
+
this.LOW_CONFIDENCE = 0.4;
|
|
29
|
+
this.UNKNOWN_CONFIDENCE = 0.0;
|
|
30
|
+
|
|
31
|
+
// Map from ConfidenceLevel enum to threshold values
|
|
32
|
+
this.CONFIDENCE_THRESHOLDS = {
|
|
33
|
+
[ConfidenceLevel.HIGH]: this.HIGH_CONFIDENCE,
|
|
34
|
+
[ConfidenceLevel.MEDIUM]: this.MEDIUM_CONFIDENCE,
|
|
35
|
+
[ConfidenceLevel.LOW]: this.LOW_CONFIDENCE,
|
|
36
|
+
[ConfidenceLevel.UNKNOWN]: this.UNKNOWN_CONFIDENCE
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Maximum pixel count to analyze for performance (will downsample larger textures)
|
|
40
|
+
this.MAX_SAMPLES = 100000;
|
|
41
|
+
|
|
42
|
+
// Statistical feature weights for each texture type
|
|
43
|
+
this.featureWeights = {
|
|
44
|
+
'base_color': {
|
|
45
|
+
'colorVariance': 0.25,
|
|
46
|
+
'rgbDistribution': 0.20,
|
|
47
|
+
'blueChannelAvg': -0.15,
|
|
48
|
+
'channelCorrelation': 0.10,
|
|
49
|
+
'entropy': 0.20,
|
|
50
|
+
'grayness': -0.10
|
|
51
|
+
},
|
|
52
|
+
'normal_map': {
|
|
53
|
+
'blueChannelBias': 0.30,
|
|
54
|
+
'rgbMeansNormal': 0.25,
|
|
55
|
+
'normalVectorValidity': 0.25,
|
|
56
|
+
'rgbDistribution': -0.10,
|
|
57
|
+
'colorVariance': 0.10
|
|
58
|
+
},
|
|
59
|
+
'orm_map': {
|
|
60
|
+
'channelIndependence': 0.25,
|
|
61
|
+
'binaryMetallicScore': 0.20,
|
|
62
|
+
'roughnessPattern': 0.15,
|
|
63
|
+
'entropy': -0.10,
|
|
64
|
+
'grayness': 0.15,
|
|
65
|
+
'colorFlatness': 0.15
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Classify the texture type of the given image
|
|
72
|
+
* @param {HTMLImageElement|ImageData|Uint8ClampedArray|URL} image - Image to classify
|
|
73
|
+
* @returns {Object} Classification results
|
|
74
|
+
*/
|
|
75
|
+
async classifyTexture(image) {
|
|
76
|
+
try {
|
|
77
|
+
// Load and process the image
|
|
78
|
+
const imgData = await this._loadImage(image);
|
|
79
|
+
if (!imgData) {
|
|
80
|
+
return {
|
|
81
|
+
classification: 'invalid_file',
|
|
82
|
+
confidence: 1.0,
|
|
83
|
+
scores: {},
|
|
84
|
+
features: {},
|
|
85
|
+
message: 'File could not be loaded as an image'
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Extract features
|
|
90
|
+
const features = this._extractFeatures(imgData);
|
|
91
|
+
|
|
92
|
+
// Calculate scores for each texture type
|
|
93
|
+
const scores = this._calculateScores(features);
|
|
94
|
+
|
|
95
|
+
// Determine the classification and confidence
|
|
96
|
+
const [classification, confidence] = this._determineClassification(scores);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
classification,
|
|
100
|
+
confidence,
|
|
101
|
+
scores,
|
|
102
|
+
features
|
|
103
|
+
};
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return {
|
|
106
|
+
classification: 'error',
|
|
107
|
+
confidence: 0.0,
|
|
108
|
+
scores: {},
|
|
109
|
+
features: {},
|
|
110
|
+
message: `Error during classification: ${error.message}`
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Load an image from various sources and return its pixel data
|
|
117
|
+
* @param {HTMLImageElement|ImageData|Uint8ClampedArray|URL|string} source - Image source
|
|
118
|
+
* @returns {Promise<ImageData>} Image data for analysis
|
|
119
|
+
*/
|
|
120
|
+
async _loadImage(source) {
|
|
121
|
+
try {
|
|
122
|
+
// Create canvas for image processing
|
|
123
|
+
const canvas = document.createElement('canvas');
|
|
124
|
+
const ctx = canvas.getContext('2d');
|
|
125
|
+
let img;
|
|
126
|
+
|
|
127
|
+
// Handle different input types
|
|
128
|
+
if (source instanceof HTMLImageElement) {
|
|
129
|
+
img = source;
|
|
130
|
+
} else if (typeof source === 'string' || source instanceof URL) {
|
|
131
|
+
// Load image from URL or path
|
|
132
|
+
img = new Image();
|
|
133
|
+
img.crossOrigin = 'Anonymous';
|
|
134
|
+
|
|
135
|
+
// Create a promise to handle async image loading
|
|
136
|
+
await new Promise((resolve, reject) => {
|
|
137
|
+
img.onload = resolve;
|
|
138
|
+
img.onerror = reject;
|
|
139
|
+
img.src = source;
|
|
140
|
+
});
|
|
141
|
+
} else if (source instanceof ImageData || source instanceof Uint8ClampedArray) {
|
|
142
|
+
// Return existing image data
|
|
143
|
+
return source instanceof ImageData ? source : new ImageData(source, 1);
|
|
144
|
+
} else {
|
|
145
|
+
throw new Error('Unsupported image source');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Resize large images for performance
|
|
149
|
+
let width = img.width;
|
|
150
|
+
let height = img.height;
|
|
151
|
+
const totalPixels = width * height;
|
|
152
|
+
|
|
153
|
+
if (totalPixels > this.MAX_SAMPLES) {
|
|
154
|
+
const scale = Math.sqrt(this.MAX_SAMPLES / totalPixels);
|
|
155
|
+
width = Math.floor(width * scale);
|
|
156
|
+
height = Math.floor(height * scale);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Set canvas dimensions and draw the image
|
|
160
|
+
canvas.width = width;
|
|
161
|
+
canvas.height = height;
|
|
162
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
163
|
+
|
|
164
|
+
// Get image data
|
|
165
|
+
return ctx.getImageData(0, 0, width, height);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error('Error loading image:', error);
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Extract statistical features from image data
|
|
174
|
+
* @param {ImageData} imgData - Image data
|
|
175
|
+
* @returns {Object} Features used for classification
|
|
176
|
+
*/
|
|
177
|
+
_extractFeatures(imgData) {
|
|
178
|
+
const { data, width, height } = imgData;
|
|
179
|
+
const pixelCount = width * height;
|
|
180
|
+
|
|
181
|
+
// Extract R, G, B channels
|
|
182
|
+
const channels = [[], [], []];
|
|
183
|
+
|
|
184
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
185
|
+
channels[0].push(data[i] / 255.0); // R
|
|
186
|
+
channels[1].push(data[i + 1] / 255.0); // G
|
|
187
|
+
channels[2].push(data[i + 2] / 255.0); // B
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const [r, g, b] = channels;
|
|
191
|
+
|
|
192
|
+
// Calculate means
|
|
193
|
+
const rMean = this._mean(r);
|
|
194
|
+
const gMean = this._mean(g);
|
|
195
|
+
const bMean = this._mean(b);
|
|
196
|
+
|
|
197
|
+
// Calculate standard deviations
|
|
198
|
+
const rStd = this._standardDeviation(r, rMean);
|
|
199
|
+
const gStd = this._standardDeviation(g, gMean);
|
|
200
|
+
const bStd = this._standardDeviation(b, bMean);
|
|
201
|
+
|
|
202
|
+
// Calculate histograms (25 bins)
|
|
203
|
+
const rHist = this._histogram(r, 25);
|
|
204
|
+
const gHist = this._histogram(g, 25);
|
|
205
|
+
const bHist = this._histogram(b, 25);
|
|
206
|
+
|
|
207
|
+
// Calculate correlation between channels
|
|
208
|
+
const rgCorr = this._correlation(r, g);
|
|
209
|
+
const rbCorr = this._correlation(r, b);
|
|
210
|
+
const gbCorr = this._correlation(g, b);
|
|
211
|
+
|
|
212
|
+
// Calculate average correlation
|
|
213
|
+
const avgCorrelation = (Math.abs(rgCorr) + Math.abs(rbCorr) + Math.abs(gbCorr)) / 3;
|
|
214
|
+
|
|
215
|
+
// Calculate blue channel bias (common in normal maps)
|
|
216
|
+
const blueHighRatio = b.filter(val => val > 0.8).length / b.length;
|
|
217
|
+
|
|
218
|
+
// Check for normal map pattern (rgb around 0.5, 0.5, 1.0)
|
|
219
|
+
let normalPatternCount = 0;
|
|
220
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
221
|
+
const distSquared = Math.pow(r[i] - 0.5, 2) + Math.pow(g[i] - 0.5, 2) + Math.pow(b[i] - 1.0, 2);
|
|
222
|
+
if (distSquared < 0.2) {
|
|
223
|
+
normalPatternCount++;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const normalPatternScore = normalPatternCount / pixelCount;
|
|
227
|
+
|
|
228
|
+
// Check for normal vector validity
|
|
229
|
+
let validNormalVectorsCount = 0;
|
|
230
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
231
|
+
// Convert from [0,1] to [-1,1]
|
|
232
|
+
const rx = r[i] * 2 - 1;
|
|
233
|
+
const gy = g[i] * 2 - 1;
|
|
234
|
+
const bz = b[i] * 2 - 1;
|
|
235
|
+
|
|
236
|
+
// Calculate magnitude
|
|
237
|
+
const magnitude = Math.sqrt(rx*rx + gy*gy + bz*bz);
|
|
238
|
+
|
|
239
|
+
// Valid vectors should have magnitude close to 1
|
|
240
|
+
if (magnitude > 0.5 && magnitude < 1.5) {
|
|
241
|
+
validNormalVectorsCount++;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const validNormalVectors = validNormalVectorsCount / pixelCount;
|
|
245
|
+
|
|
246
|
+
// Calculate color variance
|
|
247
|
+
const colorVariance = (rStd + gStd + bStd) / 3;
|
|
248
|
+
|
|
249
|
+
// Calculate grayness (how close r,g,b values are to each other)
|
|
250
|
+
let graynessSum = 0;
|
|
251
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
252
|
+
graynessSum += Math.abs(r[i] - g[i]) + Math.abs(r[i] - b[i]) + Math.abs(g[i] - b[i]);
|
|
253
|
+
}
|
|
254
|
+
const grayness = 1.0 - (graynessSum / (2.0 * pixelCount));
|
|
255
|
+
|
|
256
|
+
// Calculate entropy of each channel
|
|
257
|
+
const rEntropy = this._entropy(rHist);
|
|
258
|
+
const gEntropy = this._entropy(gHist);
|
|
259
|
+
const bEntropy = this._entropy(bHist);
|
|
260
|
+
const avgEntropy = (rEntropy + gEntropy + bEntropy) / 3;
|
|
261
|
+
|
|
262
|
+
// Calculate channel independence
|
|
263
|
+
const channelIndependence = 1.0 - avgCorrelation;
|
|
264
|
+
|
|
265
|
+
// Check for metallicity pattern (common in ORM maps)
|
|
266
|
+
const metallicPattern = b.filter(val => val < 0.12 || val > 0.88).length / b.length;
|
|
267
|
+
|
|
268
|
+
// Estimate binary metallic score
|
|
269
|
+
// This is simpler than the Laplacian in Python version but effective
|
|
270
|
+
let edgeCount = 0;
|
|
271
|
+
for (let y = 1; y < height - 1; y++) {
|
|
272
|
+
for (let x = 1; x < width - 1; x++) {
|
|
273
|
+
const idx = (y * width + x);
|
|
274
|
+
const center = b[idx];
|
|
275
|
+
const left = b[idx - 1];
|
|
276
|
+
const right = b[idx + 1];
|
|
277
|
+
const up = b[idx - width];
|
|
278
|
+
const down = b[idx + width];
|
|
279
|
+
|
|
280
|
+
// Simple edge detection
|
|
281
|
+
if (Math.abs(center - left) > 0.2 ||
|
|
282
|
+
Math.abs(center - right) > 0.2 ||
|
|
283
|
+
Math.abs(center - up) > 0.2 ||
|
|
284
|
+
Math.abs(center - down) > 0.2) {
|
|
285
|
+
edgeCount++;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const edgeRatio = edgeCount / (pixelCount * 0.25); // normalize
|
|
290
|
+
const binaryMetallicScore = metallicPattern * (1.0 / (1 + edgeRatio));
|
|
291
|
+
|
|
292
|
+
// Check for flat areas (common in roughness maps)
|
|
293
|
+
let gradientSum = 0;
|
|
294
|
+
for (let y = 1; y < height - 1; y++) {
|
|
295
|
+
for (let x = 1; x < width - 1; x++) {
|
|
296
|
+
const idx = (y * width + x);
|
|
297
|
+
const gx = Math.abs(g[idx + 1] - g[idx - 1]);
|
|
298
|
+
const gy = Math.abs(g[idx + width] - g[idx - width]);
|
|
299
|
+
gradientSum += gx + gy;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const avgGradient = gradientSum / ((width - 2) * (height - 2) * 2);
|
|
303
|
+
const roughnessPattern = 1.0 - Math.min(avgGradient * 10.0, 1.0);
|
|
304
|
+
|
|
305
|
+
// Calculate overall flatness of colors
|
|
306
|
+
const colorFlatness = 1.0 - Math.min(1.0, colorVariance * 4);
|
|
307
|
+
|
|
308
|
+
// Calculate how similar the RGB distribution is to common texture types
|
|
309
|
+
const rgbDistNormal = 1.0 - Math.sqrt(
|
|
310
|
+
Math.pow(rMean - 0.5, 2) +
|
|
311
|
+
Math.pow(gMean - 0.5, 2) +
|
|
312
|
+
Math.pow(bMean - 1.0, 2)
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// Assemble feature dictionary
|
|
316
|
+
return {
|
|
317
|
+
rMean,
|
|
318
|
+
gMean,
|
|
319
|
+
bMean,
|
|
320
|
+
rStd,
|
|
321
|
+
gStd,
|
|
322
|
+
bStd,
|
|
323
|
+
colorVariance,
|
|
324
|
+
grayness,
|
|
325
|
+
avgEntropy: avgEntropy,
|
|
326
|
+
channelCorrelation: avgCorrelation,
|
|
327
|
+
channelIndependence,
|
|
328
|
+
blueChannelBias: blueHighRatio,
|
|
329
|
+
rgbMeansNormal: rgbDistNormal,
|
|
330
|
+
normalVectorValidity: validNormalVectors,
|
|
331
|
+
binaryMetallicScore,
|
|
332
|
+
roughnessPattern,
|
|
333
|
+
colorFlatness,
|
|
334
|
+
blueChannelAvg: bMean,
|
|
335
|
+
rgbDistribution: 1 - Math.abs(rMean - gMean) - Math.abs(gMean - bMean) - Math.abs(rMean - bMean),
|
|
336
|
+
normalPatternScore
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Calculate scores for each texture type based on extracted features
|
|
342
|
+
* @param {Object} features - Features extracted from the image
|
|
343
|
+
* @returns {Object} Scores for each texture type
|
|
344
|
+
*/
|
|
345
|
+
_calculateScores(features) {
|
|
346
|
+
const scores = {};
|
|
347
|
+
|
|
348
|
+
for (const [textureType, weights] of Object.entries(this.featureWeights)) {
|
|
349
|
+
let score = 0.0;
|
|
350
|
+
|
|
351
|
+
for (const [featureName, weight] of Object.entries(weights)) {
|
|
352
|
+
if (featureName in features) {
|
|
353
|
+
score += features[featureName] * weight;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Normalize to 0-1 range
|
|
358
|
+
scores[textureType] = Math.max(0.0, Math.min(1.0, score));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return scores;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Determine the final classification and confidence level
|
|
366
|
+
* @param {Object} scores - Scores for each texture type
|
|
367
|
+
* @returns {Array} [classification, confidence]
|
|
368
|
+
*/
|
|
369
|
+
_determineClassification(scores) {
|
|
370
|
+
// Find the texture type with the highest score
|
|
371
|
+
let maxType = '';
|
|
372
|
+
let maxScore = -1;
|
|
373
|
+
|
|
374
|
+
for (const [type, score] of Object.entries(scores)) {
|
|
375
|
+
if (score > maxScore) {
|
|
376
|
+
maxScore = score;
|
|
377
|
+
maxType = type;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Check if there's a clear winner
|
|
382
|
+
const sortedScores = Object.values(scores).sort((a, b) => b - a);
|
|
383
|
+
let scoreDiff = 0;
|
|
384
|
+
|
|
385
|
+
if (sortedScores.length > 1) {
|
|
386
|
+
scoreDiff = sortedScores[0] - sortedScores[1];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Adjust confidence based on the score and difference from next best
|
|
390
|
+
const baseConfidence = maxScore;
|
|
391
|
+
const diffFactor = Math.min(scoreDiff * 3.0, 0.3); // Max 0.3 boost from diff
|
|
392
|
+
const confidence = Math.min(1.0, baseConfidence + diffFactor);
|
|
393
|
+
|
|
394
|
+
// Determine if we should classify or return unknown
|
|
395
|
+
if (confidence >= this.HIGH_CONFIDENCE) {
|
|
396
|
+
return [maxType, confidence];
|
|
397
|
+
} else if (confidence >= this.MEDIUM_CONFIDENCE) {
|
|
398
|
+
return [maxType, confidence];
|
|
399
|
+
} else if (confidence >= this.LOW_CONFIDENCE) {
|
|
400
|
+
return [`likely_${maxType}`, confidence];
|
|
401
|
+
} else {
|
|
402
|
+
return ["unknown", confidence];
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Calculate mean of an array
|
|
408
|
+
* @param {Array<number>} arr - Input array
|
|
409
|
+
* @returns {number} Mean value
|
|
410
|
+
*/
|
|
411
|
+
_mean(arr) {
|
|
412
|
+
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Calculate standard deviation of an array
|
|
417
|
+
* @param {Array<number>} arr - Input array
|
|
418
|
+
* @param {number} mean - Mean value (optional)
|
|
419
|
+
* @returns {number} Standard deviation
|
|
420
|
+
*/
|
|
421
|
+
_standardDeviation(arr, mean = null) {
|
|
422
|
+
const mu = mean !== null ? mean : this._mean(arr);
|
|
423
|
+
const squareDiffs = arr.map(value => Math.pow(value - mu, 2));
|
|
424
|
+
return Math.sqrt(this._mean(squareDiffs));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Calculate correlation between two arrays
|
|
429
|
+
* @param {Array<number>} x - First array
|
|
430
|
+
* @param {Array<number>} y - Second array
|
|
431
|
+
* @returns {number} Correlation coefficient
|
|
432
|
+
*/
|
|
433
|
+
_correlation(x, y) {
|
|
434
|
+
const xMean = this._mean(x);
|
|
435
|
+
const yMean = this._mean(y);
|
|
436
|
+
let num = 0;
|
|
437
|
+
let xSumSq = 0;
|
|
438
|
+
let ySumSq = 0;
|
|
439
|
+
|
|
440
|
+
for (let i = 0; i < x.length; i++) {
|
|
441
|
+
const xDiff = x[i] - xMean;
|
|
442
|
+
const yDiff = y[i] - yMean;
|
|
443
|
+
num += xDiff * yDiff;
|
|
444
|
+
xSumSq += xDiff * xDiff;
|
|
445
|
+
ySumSq += yDiff * yDiff;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return num / (Math.sqrt(xSumSq) * Math.sqrt(ySumSq) || 1);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Calculate histogram for an array
|
|
453
|
+
* @param {Array<number>} arr - Input array
|
|
454
|
+
* @param {number} bins - Number of bins
|
|
455
|
+
* @returns {Array<number>} Histogram
|
|
456
|
+
*/
|
|
457
|
+
_histogram(arr, bins) {
|
|
458
|
+
const hist = new Array(bins).fill(0);
|
|
459
|
+
const binSize = 1.0 / bins;
|
|
460
|
+
|
|
461
|
+
for (const value of arr) {
|
|
462
|
+
const binIndex = Math.min(Math.floor(value / binSize), bins - 1);
|
|
463
|
+
hist[binIndex]++;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Normalize
|
|
467
|
+
return hist.map(h => h / arr.length);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Calculate entropy from a histogram
|
|
472
|
+
* @param {Array<number>} hist - Histogram
|
|
473
|
+
* @returns {number} Entropy
|
|
474
|
+
*/
|
|
475
|
+
_entropy(hist) {
|
|
476
|
+
return -hist
|
|
477
|
+
.filter(p => p > 0)
|
|
478
|
+
.reduce((sum, p) => sum + p * Math.log2(p), 0);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Classify a list of atlas files and determine their texture types
|
|
484
|
+
* @param {Array<string|HTMLImageElement|ImageData>} files - List of image sources
|
|
485
|
+
* @param {Object} options - Options
|
|
486
|
+
* @param {boolean} [options.verbose=false] - Whether to include detailed feature and score information
|
|
487
|
+
* @param {string} [options.acceptableConfidence=null] - Override the global ACCEPTABLE_CONFIDENCE level
|
|
488
|
+
* @returns {Promise<Object>} Classification results
|
|
489
|
+
*/
|
|
490
|
+
export async function classifyAtlasFiles(files, options = {}) {
|
|
491
|
+
const classifier = new TextureClassifier();
|
|
492
|
+
const { verbose = false, acceptableConfidence = null } = options;
|
|
493
|
+
|
|
494
|
+
// Use the provided confidence level or fall back to the global constant
|
|
495
|
+
const confidenceLevel = acceptableConfidence || ACCEPTABLE_CONFIDENCE;
|
|
496
|
+
const minConfidenceThreshold = classifier.CONFIDENCE_THRESHOLDS[confidenceLevel];
|
|
497
|
+
|
|
498
|
+
// Classify each file
|
|
499
|
+
const fileClassifications = [];
|
|
500
|
+
|
|
501
|
+
for (const fileSource of files) {
|
|
502
|
+
let fileName = "";
|
|
503
|
+
if (typeof fileSource === 'string') {
|
|
504
|
+
// Extract filename from path or URL
|
|
505
|
+
fileName = fileSource.split('/').pop().split('\\').pop();
|
|
506
|
+
} else if (fileSource instanceof File) {
|
|
507
|
+
fileName = fileSource.name;
|
|
508
|
+
} else {
|
|
509
|
+
fileName = "unnamed_texture";
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const result = await classifier.classifyTexture(fileSource);
|
|
513
|
+
result.file = fileName;
|
|
514
|
+
result.path = fileSource;
|
|
515
|
+
fileClassifications.push(result);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Assign texture types based on classification results
|
|
519
|
+
const bestMatches = {};
|
|
520
|
+
const assignedFiles = new Set();
|
|
521
|
+
|
|
522
|
+
// First pass: Assign high confidence matches
|
|
523
|
+
for (const textureType of ['base_color', 'normal_map', 'orm_map']) {
|
|
524
|
+
const candidates = fileClassifications.filter(res =>
|
|
525
|
+
res.classification === textureType &&
|
|
526
|
+
res.confidence >= classifier.HIGH_CONFIDENCE &&
|
|
527
|
+
!assignedFiles.has(res.path)
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
if (candidates.length > 0) {
|
|
531
|
+
// Sort by confidence
|
|
532
|
+
candidates.sort((a, b) => b.confidence - a.confidence);
|
|
533
|
+
const bestMatch = candidates[0];
|
|
534
|
+
bestMatches[textureType] = {
|
|
535
|
+
file: bestMatch.file,
|
|
536
|
+
path: bestMatch.path,
|
|
537
|
+
confidence: bestMatch.confidence,
|
|
538
|
+
confidenceLevel: 'high'
|
|
539
|
+
};
|
|
540
|
+
assignedFiles.add(bestMatch.path);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Second pass: Handle medium confidence matches if they meet the acceptable threshold
|
|
545
|
+
if (minConfidenceThreshold <= classifier.MEDIUM_CONFIDENCE) {
|
|
546
|
+
for (const textureType of ['base_color', 'normal_map', 'orm_map']) {
|
|
547
|
+
if (textureType in bestMatches) continue;
|
|
548
|
+
|
|
549
|
+
const candidates = fileClassifications.filter(res =>
|
|
550
|
+
(res.classification === textureType || res.classification === `likely_${textureType}`) &&
|
|
551
|
+
res.confidence >= classifier.MEDIUM_CONFIDENCE &&
|
|
552
|
+
!assignedFiles.has(res.path)
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
if (candidates.length > 0) {
|
|
556
|
+
candidates.sort((a, b) => b.confidence - a.confidence);
|
|
557
|
+
const bestMatch = candidates[0];
|
|
558
|
+
bestMatches[textureType] = {
|
|
559
|
+
file: bestMatch.file,
|
|
560
|
+
path: bestMatch.path,
|
|
561
|
+
confidence: bestMatch.confidence,
|
|
562
|
+
confidenceLevel: 'medium',
|
|
563
|
+
uncertain: true
|
|
564
|
+
};
|
|
565
|
+
assignedFiles.add(bestMatch.path);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Third pass: Handle low confidence matches if they meet the acceptable threshold
|
|
571
|
+
if (minConfidenceThreshold <= classifier.LOW_CONFIDENCE) {
|
|
572
|
+
for (const textureType of ['base_color', 'normal_map', 'orm_map']) {
|
|
573
|
+
if (textureType in bestMatches) continue;
|
|
574
|
+
|
|
575
|
+
const candidates = fileClassifications.filter(res =>
|
|
576
|
+
((res.scores[textureType] || 0) >= classifier.LOW_CONFIDENCE) &&
|
|
577
|
+
!assignedFiles.has(res.path)
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
if (candidates.length > 0) {
|
|
581
|
+
candidates.sort((a, b) => (b.scores[textureType] || 0) - (a.scores[textureType] || 0));
|
|
582
|
+
const bestMatch = candidates[0];
|
|
583
|
+
bestMatches[textureType] = {
|
|
584
|
+
file: bestMatch.file,
|
|
585
|
+
path: bestMatch.path,
|
|
586
|
+
confidence: bestMatch.scores[textureType] || 0,
|
|
587
|
+
confidenceLevel: 'low',
|
|
588
|
+
bestGuess: true
|
|
589
|
+
};
|
|
590
|
+
assignedFiles.add(bestMatch.path);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Handle remaining unassigned files as best guesses only if we're accepting UNKNOWN confidence
|
|
596
|
+
if (minConfidenceThreshold <= classifier.UNKNOWN_CONFIDENCE) {
|
|
597
|
+
const remainingFiles = fileClassifications.filter(res => !assignedFiles.has(res.path));
|
|
598
|
+
|
|
599
|
+
for (const res of remainingFiles) {
|
|
600
|
+
// Get the highest score texture type
|
|
601
|
+
let maxType = '';
|
|
602
|
+
let maxScore = -1;
|
|
603
|
+
|
|
604
|
+
for (const [type, score] of Object.entries(res.scores)) {
|
|
605
|
+
if (score > maxScore) {
|
|
606
|
+
maxScore = score;
|
|
607
|
+
maxType = type;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (maxType && !(maxType in bestMatches)) {
|
|
612
|
+
bestMatches[maxType] = {
|
|
613
|
+
file: res.file,
|
|
614
|
+
path: res.path,
|
|
615
|
+
confidence: res.scores[maxType],
|
|
616
|
+
confidenceLevel: 'unknown',
|
|
617
|
+
bestGuess: true
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Mark missing texture types
|
|
624
|
+
for (const missingType of ['base_color', 'normal_map', 'orm_map']) {
|
|
625
|
+
if (!(missingType in bestMatches)) {
|
|
626
|
+
bestMatches[missingType] = {
|
|
627
|
+
file: null,
|
|
628
|
+
path: null,
|
|
629
|
+
confidence: 0.0,
|
|
630
|
+
missing: true
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Prepare the final result
|
|
636
|
+
const results = {
|
|
637
|
+
textureAssignments: bestMatches,
|
|
638
|
+
acceptableConfidenceLevel: confidenceLevel,
|
|
639
|
+
summary: {
|
|
640
|
+
baseColor: bestMatches['base_color'].file || "No file",
|
|
641
|
+
normalMap: bestMatches['normal_map'].file || "No file",
|
|
642
|
+
ormMap: bestMatches['orm_map'].file || "No file"
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
if (verbose) {
|
|
647
|
+
results.detailedClassifications = fileClassifications;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return results;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Set the global acceptable confidence level
|
|
655
|
+
* @param {string} level - Confidence level ('high', 'medium', 'low', 'unknown')
|
|
656
|
+
*/
|
|
657
|
+
export function setAcceptableConfidence(level) {
|
|
658
|
+
if (Object.values(ConfidenceLevel).includes(level)) {
|
|
659
|
+
ACCEPTABLE_CONFIDENCE = level;
|
|
660
|
+
} else {
|
|
661
|
+
throw new Error(`Invalid confidence level: ${level}. Must be one of: high, medium, low, unknown`);
|
|
662
|
+
}
|
|
663
|
+
}
|