fix(oom): use mmap=True for checkpoint loading + malloc_trim + expandable_segments

Root cause: torch.load() reads 6.9GB .ckpt into Python heap + model params
in CPU RAM = ~14GB peak, exceeding 16GB system RAM → OOM Killer.

Fix 1 - mmap=True on all torch.load() calls (torch 2.7 supports this):
  With mmap, checkpoint storage is file-backed (not heap). Only the model
  parameters (also ~7GB) exist in physical RAM during loading. Peak RAM
  drops from ~14GB to ~7GB — within safe limits on 16GB machines.
  Files changed: pipelines.py, hunyuan3ddit.py, model.py (×2), flow_matching_sit.py

Fix 2 - malloc_trim(0) after every gc.collect():
  Forces glibc to return freed heap pages to OS immediately, so Python's
  memory pool doesn't hoard freed model memory before the next load.

Fix 3 - PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True:
  Prevents CUDA allocator fragmentation between model switches.

Fix 4 - Adaptive threshold recalculated:
  With mmap loading, loading a model requires ~7.5GB (model params) not
  14GB. CPU offload threshold lowered from 16GB → 10.5GB, enabling fast
  path on machines with more headroom.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Akasei
2026-03-16 23:18:16 +08:00
parent 6534f4ba15
commit f192c86c60
46 changed files with 334079 additions and 10 deletions

View File

@@ -34,6 +34,8 @@ import random
import shutil import shutil
import subprocess import subprocess
import time import time
import ctypes
import ctypes.util
from glob import glob from glob import glob
from pathlib import Path from pathlib import Path
@@ -49,6 +51,18 @@ import numpy as np
from hy3dshape.utils import logger from hy3dshape.utils import logger
from hy3dpaint.convert_utils import create_glb_with_pbr_materials from hy3dpaint.convert_utils import create_glb_with_pbr_materials
# Force OS to reclaim freed heap pages, reducing Python's RSS after model deletion.
_libc = ctypes.CDLL(ctypes.util.find_library("c") or "libc.so.6", use_errno=True)
def _malloc_trim():
try:
_libc.malloc_trim(0)
except Exception:
pass
# Allow CUDA allocator to use expandable segments, reducing fragmentation.
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
# Globals for lazy load/unload # Globals for lazy load/unload
i23d_worker = None i23d_worker = None
tex_pipeline = None tex_pipeline = None
@@ -237,9 +251,10 @@ height="{height}" width="100%" frameborder="0"></iframe>'
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Approximate RAM required (GB) to hold one model in CPU while loading another. # Approximate RAM required (GB) to hold one model in CPU while loading another.
# Model weights: ~7GB each. Loading from disk stages ~7GB temporarily. # With mmap=True loading, staging a model needs ~0 extra heap RAM.
# Total: 7 (existing in CPU) + 7 (loading new) + 2 (OS headroom) = 16GB. # So threshold = size of model in CPU RAM = ~7.5GB, plus 3GB headroom = 10.5GB.
_RAM_THRESHOLD_GB = 16.0 # With 16GB total, we need at least ~10.5GB free to safely offload i23d to CPU.
_RAM_THRESHOLD_GB = 10.5
# Track whether i23d is offloaded to CPU RAM (vs deleted entirely). # Track whether i23d is offloaded to CPU RAM (vs deleted entirely).
_i23d_on_cpu = False _i23d_on_cpu = False
@@ -258,12 +273,12 @@ def _get_available_ram_gb():
def _can_offload_to_cpu(): def _can_offload_to_cpu():
"""Check if there's enough RAM to keep a model in CPU while loading another.""" """Check if there's enough RAM to keep i23d in CPU while loading tex."""
available = _get_available_ram_gb() available = _get_available_ram_gb()
can = available >= _RAM_THRESHOLD_GB can = available >= _RAM_THRESHOLD_GB
logger.info( logger.info(
f"RAM check: {available:.1f}GB available, " f"RAM check: {available:.1f}GB available, "
f"need {_RAM_THRESHOLD_GB:.0f}GB for CPU offload → " f"need {_RAM_THRESHOLD_GB:.1f}GB for CPU offload → "
f"{'CPU offload (fast)' if can else 'full delete (safe)'}" f"{'CPU offload (fast)' if can else 'full delete (safe)'}"
) )
return can return can
@@ -280,6 +295,8 @@ def _prepare_for_tex():
logger.info("Offloading shape model to CPU RAM (fast path)...") logger.info("Offloading shape model to CPU RAM (fast path)...")
i23d_worker.to('cpu') i23d_worker.to('cpu')
_i23d_on_cpu = True _i23d_on_cpu = True
gc.collect()
_malloc_trim()
torch.cuda.empty_cache() torch.cuda.empty_cache()
else: else:
logger.info("Deleting shape model entirely (safe path, limited RAM)...") logger.info("Deleting shape model entirely (safe path, limited RAM)...")
@@ -288,6 +305,7 @@ def _prepare_for_tex():
_i23d_on_cpu = False _i23d_on_cpu = False
gc.collect() gc.collect()
gc.collect() gc.collect()
_malloc_trim()
torch.cuda.empty_cache() torch.cuda.empty_cache()
_ensure_tex_pipeline() _ensure_tex_pipeline()
@@ -303,6 +321,7 @@ def _ensure_i23d_worker():
elif i23d_worker is None: elif i23d_worker is None:
logger.info("Reloading shape model from disk to GPU (slow path)...") logger.info("Reloading shape model from disk to GPU (slow path)...")
gc.collect() gc.collect()
_malloc_trim()
torch.cuda.empty_cache() torch.cuda.empty_cache()
from hy3dshape import Hunyuan3DDiTFlowMatchingPipeline from hy3dshape import Hunyuan3DDiTFlowMatchingPipeline
i23d_worker = Hunyuan3DDiTFlowMatchingPipeline.from_pretrained( i23d_worker = Hunyuan3DDiTFlowMatchingPipeline.from_pretrained(
@@ -324,6 +343,7 @@ def _unload_tex_pipeline():
tex_pipeline = None tex_pipeline = None
gc.collect() gc.collect()
gc.collect() gc.collect()
_malloc_trim()
torch.cuda.empty_cache() torch.cuda.empty_cache()
@@ -332,6 +352,7 @@ def _ensure_tex_pipeline():
global tex_pipeline global tex_pipeline
if tex_pipeline is None and tex_conf is not None: if tex_pipeline is None and tex_conf is not None:
gc.collect() gc.collect()
_malloc_trim()
torch.cuda.empty_cache() torch.cuda.empty_cache()
from hy3dpaint.textureGenPipeline import Hunyuan3DPaintPipeline from hy3dpaint.textureGenPipeline import Hunyuan3DPaintPipeline
logger.info("Loading texture pipeline to GPU...") logger.info("Loading texture pipeline to GPU...")

View File

@@ -143,7 +143,7 @@ class VectsetVAE(nn.Module):
import safetensors.torch import safetensors.torch
ckpt = safetensors.torch.load_file(ckpt_path, device='cpu') ckpt = safetensors.torch.load_file(ckpt_path, device='cpu')
else: else:
ckpt = torch.load(ckpt_path, map_location='cpu', weights_only=True) ckpt = torch.load(ckpt_path, map_location='cpu', weights_only=True, mmap=True)
model_kwargs = config['params'] model_kwargs = config['params']
model_kwargs.update(kwargs) model_kwargs.update(kwargs)
@@ -181,7 +181,7 @@ class VectsetVAE(nn.Module):
) )
def init_from_ckpt(self, path, ignore_keys=()): def init_from_ckpt(self, path, ignore_keys=()):
state_dict = torch.load(path, map_location="cpu") state_dict = torch.load(path, map_location="cpu", mmap=True)
state_dict = state_dict.get("state_dict", state_dict) state_dict = state_dict.get("state_dict", state_dict)
keys = list(state_dict.keys()) keys = list(state_dict.keys())
for k in keys: for k in keys:

View File

@@ -358,7 +358,7 @@ class Hunyuan3DDiT(nn.Module):
if ckpt_path is not None: if ckpt_path is not None:
print('restored denoiser ckpt', ckpt_path) print('restored denoiser ckpt', ckpt_path)
ckpt = torch.load(ckpt_path, map_location="cpu") ckpt = torch.load(ckpt_path, map_location='cpu', mmap=True)
if 'state_dict' not in ckpt: if 'state_dict' not in ckpt:
# deepspeed ckpt # deepspeed ckpt
state_dict = {} state_dict = {}

View File

@@ -135,7 +135,7 @@ class Diffuser(pl.LightningModule):
print(f"{context}: Restored training weights") print(f"{context}: Restored training weights")
def init_from_ckpt(self, path, ignore_keys=()): def init_from_ckpt(self, path, ignore_keys=()):
ckpt = torch.load(path, map_location="cpu") ckpt = torch.load(path, map_location="cpu", mmap=True)
if 'state_dict' not in ckpt: if 'state_dict' not in ckpt:
# deepspeed ckpt # deepspeed ckpt
state_dict = {} state_dict = {}

View File

@@ -165,7 +165,7 @@ class Hunyuan3DDiTPipeline:
ckpt[model_name] = {} ckpt[model_name] = {}
ckpt[model_name][new_key] = value ckpt[model_name][new_key] = value
else: else:
ckpt = torch.load(ckpt_path, map_location='cpu', weights_only=True) ckpt = torch.load(ckpt_path, map_location='cpu', weights_only=True, mmap=True)
# load model # load model
model = instantiate_from_config(config['model']) model = instantiate_from_config(config['model'])
model.load_state_dict(ckpt['model']) model.load_state_dict(ckpt['model'])

BIN
mr_combined.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html>
<head>
<!-- Import the component -->
<script src="https://cdn.jsdelivr.net/npm/@google/model-viewer@3.1.1/dist/model-viewer.min.js" type="module"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const modelViewers = document.querySelectorAll('model-viewer');
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
modelViewers.forEach(modelViewer => {
modelViewer.setAttribute(
"environment-image",
"/static/env_maps/gradient.jpg"
);
// if (!isSafari) {
// modelViewer.setAttribute(
// "environment-image",
// "/static/env_maps/gradient.jpg"
// );
// } else {
// modelViewer.addEventListener('load', (event) => {
// const [material] = modelViewer.model.materials;
// let color = [43, 44, 46, 255];
// color = color.map(x => x / 255);
// material.pbrMetallicRoughness.setMetallicFactor(0.1); // 完全金属
// material.pbrMetallicRoughness.setRoughnessFactor(0.7); // 低粗糙度
// material.pbrMetallicRoughness.setBaseColorFactor(color); // CornflowerBlue in RGB
// });
// }
// modelViewer.addEventListener('load', (event) => {
// const [material] = modelViewer.model.materials;
// let color = [43, 44, 46, 255];
// color = color.map(x => x / 255);
// material.pbrMetallicRoughness.setMetallicFactor(0.1); // 完全金属
// material.pbrMetallicRoughness.setRoughnessFactor(0.7); // 低粗糙度
// material.pbrMetallicRoughness.setBaseColorFactor(color); // CornflowerBlue in RGB
// });
});
});
</script>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
}
.centered-container {
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
border-color: #e5e7eb;
border-style: solid;
border-width: 1px;
}
</style>
</head>
<body>
<div class="centered-container">
<div class="column is-mobile is-centered">
<model-viewer id="modelviewer" style="height: 640px; width: 500px;"
rotation-per-second="10deg"
src="./white_mesh.glb/" disable-tap
environment-image="neutral"
camera-target="0m 0m 0m"
camera-orbit="0deg 90deg 8m"
orientation="0deg 0deg 0deg"
shadow-intensity=".9"
ar auto-rotate
camera-controls>
</model-viewer>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html>
<head>
<!-- Import the component -->
<script src="https://cdn.jsdelivr.net/npm/@google/model-viewer@3.1.1/dist/model-viewer.min.js" type="module"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const modelViewers = document.querySelectorAll('model-viewer');
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
modelViewers.forEach(modelViewer => {
modelViewer.setAttribute(
"environment-image",
"/static/env_maps/gradient.jpg"
);
// if (!isSafari) {
// modelViewer.setAttribute(
// "environment-image",
// "/static/env_maps/gradient.jpg"
// );
// } else {
// modelViewer.addEventListener('load', (event) => {
// const [material] = modelViewer.model.materials;
// let color = [43, 44, 46, 255];
// color = color.map(x => x / 255);
// material.pbrMetallicRoughness.setMetallicFactor(0.1); // 完全金属
// material.pbrMetallicRoughness.setRoughnessFactor(0.7); // 低粗糙度
// material.pbrMetallicRoughness.setBaseColorFactor(color); // CornflowerBlue in RGB
// });
// }
// modelViewer.addEventListener('load', (event) => {
// const [material] = modelViewer.model.materials;
// let color = [43, 44, 46, 255];
// color = color.map(x => x / 255);
// material.pbrMetallicRoughness.setMetallicFactor(0.1); // 完全金属
// material.pbrMetallicRoughness.setRoughnessFactor(0.7); // 低粗糙度
// material.pbrMetallicRoughness.setBaseColorFactor(color); // CornflowerBlue in RGB
// });
});
});
</script>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
}
.centered-container {
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
border-color: #e5e7eb;
border-style: solid;
border-width: 1px;
}
</style>
</head>
<body>
<div class="centered-container">
<div class="column is-mobile is-centered">
<model-viewer id="modelviewer" style="height: 640px; width: 500px;"
rotation-per-second="10deg"
src="./white_mesh.glb/" disable-tap
environment-image="neutral"
camera-target="0m 0m 0m"
camera-orbit="0deg 90deg 8m"
orientation="0deg 0deg 0deg"
shadow-intensity=".9"
ar auto-rotate
camera-controls>
</model-viewer>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html>
<head>
<!-- Import the component -->
<script src="https://cdn.jsdelivr.net/npm/@google/model-viewer@3.1.1/dist/model-viewer.min.js" type="module"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const modelViewers = document.querySelectorAll('model-viewer');
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
modelViewers.forEach(modelViewer => {
modelViewer.setAttribute(
"environment-image",
"/static/env_maps/gradient.jpg"
);
// if (!isSafari) {
// modelViewer.setAttribute(
// "environment-image",
// "/static/env_maps/gradient.jpg"
// );
// } else {
// modelViewer.addEventListener('load', (event) => {
// const [material] = modelViewer.model.materials;
// let color = [43, 44, 46, 255];
// color = color.map(x => x / 255);
// material.pbrMetallicRoughness.setMetallicFactor(0.1); // 完全金属
// material.pbrMetallicRoughness.setRoughnessFactor(0.7); // 低粗糙度
// material.pbrMetallicRoughness.setBaseColorFactor(color); // CornflowerBlue in RGB
// });
// }
// modelViewer.addEventListener('load', (event) => {
// const [material] = modelViewer.model.materials;
// let color = [43, 44, 46, 255];
// color = color.map(x => x / 255);
// material.pbrMetallicRoughness.setMetallicFactor(0.1); // 完全金属
// material.pbrMetallicRoughness.setRoughnessFactor(0.7); // 低粗糙度
// material.pbrMetallicRoughness.setBaseColorFactor(color); // CornflowerBlue in RGB
// });
});
});
</script>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
}
.centered-container {
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
border-color: #e5e7eb;
border-style: solid;
border-width: 1px;
}
</style>
</head>
<body>
<div class="centered-container">
<div class="column is-mobile is-centered">
<model-viewer id="modelviewer" style="height: 640px; width: 500px;"
rotation-per-second="10deg"
src="./white_mesh.glb/" disable-tap
environment-image="neutral"
camera-target="0m 0m 0m"
camera-orbit="0deg 90deg 8m"
orientation="0deg 0deg 0deg"
shadow-intensity=".9"
ar auto-rotate
camera-controls>
</model-viewer>
</div>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html>
<head>
<!-- Import the component -->
<script src="https://cdn.jsdelivr.net/npm/@google/model-viewer@3.1.1/dist/model-viewer.min.js" type="module"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const modelViewers = document.querySelectorAll('model-viewer');
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
modelViewers.forEach(modelViewer => {
modelViewer.setAttribute(
"environment-image",
"/static/env_maps/gradient.jpg"
);
// if (!isSafari) {
// modelViewer.setAttribute(
// "environment-image",
// "/static/env_maps/gradient.jpg"
// );
// } else {
// modelViewer.addEventListener('load', (event) => {
// const [material] = modelViewer.model.materials;
// let color = [43, 44, 46, 255];
// color = color.map(x => x / 255);
// material.pbrMetallicRoughness.setMetallicFactor(0.1); // 完全金属
// material.pbrMetallicRoughness.setRoughnessFactor(0.7); // 低粗糙度
// material.pbrMetallicRoughness.setBaseColorFactor(color); // CornflowerBlue in RGB
// });
// }
// modelViewer.addEventListener('load', (event) => {
// const [material] = modelViewer.model.materials;
// let color = [43, 44, 46, 255];
// color = color.map(x => x / 255);
// material.pbrMetallicRoughness.setMetallicFactor(0.1); // 完全金属
// material.pbrMetallicRoughness.setRoughnessFactor(0.7); // 低粗糙度
// material.pbrMetallicRoughness.setBaseColorFactor(color); // CornflowerBlue in RGB
// });
});
});
</script>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
}
.centered-container {
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
border-color: #e5e7eb;
border-style: solid;
border-width: 1px;
}
</style>
</head>
<body>
<div class="centered-container">
<div class="column is-mobile is-centered">
<model-viewer id="modelviewer" style="height: 640px; width: 500px;"
rotation-per-second="10deg"
src="./white_mesh.glb/" disable-tap
environment-image="neutral"
camera-target="0m 0m 0m"
camera-orbit="0deg 90deg 8m"
orientation="0deg 0deg 0deg"
shadow-intensity=".9"
ar auto-rotate
camera-controls>
</model-viewer>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,321 @@
<!DOCTYPE html>
<html>
<head>
<!-- Import the component -->
<script src="https://cdn.jsdelivr.net/npm/@google/model-viewer@3.1.1/dist/model-viewer.min.js"
type="module"></script>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
}
.centered-container {
display: flex;
justify-content: center;
align-items: center;
}
.modelviewer-panel-button {
height: 30px;
margin: 4px 4px;
padding: 0px 14px;
background: white;
border-radius: 10px;
box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.25);
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.modelviewer-panel-button.checked {
background: #6567C9;
color: white;
}
.modelviewer-panel-button:hover {
background-color: #e2e6ea;
}
.modelviewer-panel-button-container {
display: flex;
justify-content: space-around;
}
.centered-container {
display: flex;
flex-direction: column;
align-items: center;
}
</style>
</head>
<body>
<div class="centered-container">
<div class="centered-container">
<div class="column is-mobile is-centered">
<model-viewer id="modelviewer" style="height: 600px; width: 500px;"
rotation-per-second="10deg"
src="./textured_mesh.glb/" disable-tap
environment-image="neutral"
camera-target="0m 0m 0m"
camera-orbit="0deg 90deg 12m"
orientation="0deg 0deg 0deg"
shadow-intensity=".9"
ar auto-rotate
camera-controls>
</model-viewer>
</div>
<div class="modelviewer-panel-button-container">
<div id="appearance-button" class="modelviewer-panel-button small checked" onclick="showTexture()">
Appearance
</div>
<div id="geometry-button" class="modelviewer-panel-button small" onclick="hideTexture()">Geometry</div>
</div>
</div>
</div>
<script>
let modelViewer;
let window_state = {
isModelLoaded: false,
isHidden: false,
savedMaterials: null,
savedExposure: null
};
document.addEventListener('DOMContentLoaded', () => {
modelViewer = document.getElementById('modelviewer');
// 等待模型加载完成
modelViewer.addEventListener('load', () => {
console.log('Model loaded, materials available:', modelViewer.model.materials.length);
window_state.isModelLoaded = true;
// 调试:打印材质信息
modelViewer.model.materials.forEach((mat, idx) => {
console.log(`Material ${idx}:`, {
name: mat.name,
hasBaseColor: !!mat.pbrMetallicRoughness.baseColorTexture,
hasMetallicRoughness: !!mat.pbrMetallicRoughness.metallicRoughnessTexture,
hasNormal: !!mat.normalTexture
});
});
});
modelViewer.addEventListener('error', (event) => {
console.error('Model loading error:', event);
});
});
function ensureModelLoaded() {
if (!window_state.isModelLoaded || !modelViewer.model || !modelViewer.model.materials) {
console.warn('Model not loaded yet');
return false;
}
return true;
}
function saveMaterialsState() {
if (!ensureModelLoaded()) return false;
console.log('Saving materials state...');
window_state.savedMaterials = [];
window_state.savedExposure = modelViewer.exposure;
for (let i = 0; i < modelViewer.model.materials.length; i++) {
const material = modelViewer.model.materials[i];
const pbr = material.pbrMetallicRoughness;
const materialState = {
baseColorTexture: null,
metallicRoughnessTexture: null,
normalTexture: null,
baseColorFactor: null,
metallicFactor: null,
roughnessFactor: null
};
// 保存纹理引用
try {
if (pbr.baseColorTexture && pbr.baseColorTexture.texture) {
materialState.baseColorTexture = pbr.baseColorTexture.texture;
console.log(`Saved baseColorTexture for material ${i}`);
}
if (pbr.metallicRoughnessTexture && pbr.metallicRoughnessTexture.texture) {
materialState.metallicRoughnessTexture = pbr.metallicRoughnessTexture.texture;
console.log(`Saved metallicRoughnessTexture for material ${i}`);
}
if (material.normalTexture && material.normalTexture.texture) {
materialState.normalTexture = material.normalTexture.texture;
console.log(`Saved normalTexture for material ${i}`);
}
// 保存材质参数
materialState.baseColorFactor = [...pbr.baseColorFactor];
materialState.metallicFactor = pbr.metallicFactor;
materialState.roughnessFactor = pbr.roughnessFactor;
} catch (error) {
console.error(`Error saving material ${i}:`, error);
}
window_state.savedMaterials.push(materialState);
}
console.log('Materials state saved:', window_state.savedMaterials);
return true;
}
function hideTexture() {
console.log('hideTexture called');
if (!ensureModelLoaded()) {
console.error('Cannot hide texture: model not loaded');
return;
}
let appearanceButton = document.getElementById('appearance-button');
let geometryButton = document.getElementById('geometry-button');
appearanceButton.classList.remove('checked');
geometryButton.classList.add('checked');
// 如果已经隐藏,直接返回
if (window_state.isHidden) return;
// 第一次隐藏时保存状态
if (!window_state.savedMaterials) {
if (!saveMaterialsState()) return;
}
// 隐藏所有纹理
try {
for (let i = 0; i < modelViewer.model.materials.length; i++) {
const material = modelViewer.model.materials[i];
const pbr = material.pbrMetallicRoughness;
if (pbr.baseColorTexture) {
pbr.baseColorTexture.setTexture(null);
}
if (pbr.metallicRoughnessTexture) {
pbr.metallicRoughnessTexture.setTexture(null);
}
if (material.normalTexture) {
material.normalTexture.setTexture(null);
}
}
window_state.isHidden = true;
modelViewer.environmentImage = '/static/env_maps/gradient.jpg';
modelViewer.exposure = 4;
console.log('Textures hidden successfully');
} catch (error) {
console.error('Error hiding textures:', error);
}
}
function showTexture() {
console.log('showTexture called');
if (!ensureModelLoaded()) {
console.error('Cannot show texture: model not loaded');
return;
}
let appearanceButton = document.getElementById('appearance-button');
let geometryButton = document.getElementById('geometry-button');
appearanceButton.classList.add('checked');
geometryButton.classList.remove('checked');
// 如果不在隐藏状态,直接返回
if (!window_state.isHidden) return;
// 如果没有保存的材质状态,无法恢复
if (!window_state.savedMaterials) {
console.warn('No saved materials to restore');
return;
}
// 恢复纹理
try {
for (let i = 0; i < modelViewer.model.materials.length && i < window_state.savedMaterials.length; i++) {
const material = modelViewer.model.materials[i];
const pbr = material.pbrMetallicRoughness;
const savedMaterial = window_state.savedMaterials[i];
// 恢复纹理
if (savedMaterial.baseColorTexture && pbr.baseColorTexture) {
pbr.baseColorTexture.setTexture(savedMaterial.baseColorTexture);
console.log(`Restored baseColorTexture for material ${i}`);
}
if (savedMaterial.metallicRoughnessTexture && pbr.metallicRoughnessTexture) {
pbr.metallicRoughnessTexture.setTexture(savedMaterial.metallicRoughnessTexture);
console.log(`Restored metallicRoughnessTexture for material ${i}`);
}
if (savedMaterial.normalTexture && material.normalTexture) {
material.normalTexture.setTexture(savedMaterial.normalTexture);
console.log(`Restored normalTexture for material ${i}`);
}
// 恢复材质参数
if (savedMaterial.baseColorFactor) {
pbr.setBaseColorFactor(savedMaterial.baseColorFactor);
}
if (typeof savedMaterial.metallicFactor === 'number') {
pbr.setMetallicFactor(savedMaterial.metallicFactor);
}
if (typeof savedMaterial.roughnessFactor === 'number') {
pbr.setRoughnessFactor(savedMaterial.roughnessFactor);
}
}
// 恢复环境设置
modelViewer.environmentImage = '/static/env_maps/white.jpg';
if (window_state.savedExposure !== undefined) {
modelViewer.exposure = window_state.savedExposure;
}
window_state.isHidden = false;
console.log('Textures restored successfully');
} catch (error) {
console.error('Error restoring textures:', error);
}
}
// 添加调试函数
function debugMaterials() {
if (!ensureModelLoaded()) return;
console.log('=== Current Materials Debug ===');
modelViewer.model.materials.forEach((mat, idx) => {
const pbr = mat.pbrMetallicRoughness;
console.log(`Material ${idx}:`, {
name: mat.name,
baseColorTexture: pbr.baseColorTexture?.texture || null,
metallicRoughnessTexture: pbr.metallicRoughnessTexture?.texture || null,
normalTexture: mat.normalTexture?.texture || null,
baseColorFactor: pbr.baseColorFactor,
metallicFactor: pbr.metallicFactor,
roughnessFactor: pbr.roughnessFactor
});
});
console.log('Window state:', window_state);
console.log('===============================');
}
// 暴露调试函数到全局
window.debugMaterials = debugMaterials;
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

View File

@@ -0,0 +1,9 @@
newmtl Material
Kd 0.8 0.8 0.8
Ke 0.0 0.0 0.0
Ni 1.5
d 1.0
illum 2
map_Kd textured_mesh.jpg
map_Pm textured_mesh_metallic.jpg
map_Pr textured_mesh_roughness.jpg

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,321 @@
<!DOCTYPE html>
<html>
<head>
<!-- Import the component -->
<script src="https://cdn.jsdelivr.net/npm/@google/model-viewer@3.1.1/dist/model-viewer.min.js"
type="module"></script>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
}
.centered-container {
display: flex;
justify-content: center;
align-items: center;
}
.modelviewer-panel-button {
height: 30px;
margin: 4px 4px;
padding: 0px 14px;
background: white;
border-radius: 10px;
box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.25);
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.modelviewer-panel-button.checked {
background: #6567C9;
color: white;
}
.modelviewer-panel-button:hover {
background-color: #e2e6ea;
}
.modelviewer-panel-button-container {
display: flex;
justify-content: space-around;
}
.centered-container {
display: flex;
flex-direction: column;
align-items: center;
}
</style>
</head>
<body>
<div class="centered-container">
<div class="centered-container">
<div class="column is-mobile is-centered">
<model-viewer id="modelviewer" style="height: 600px; width: 500px;"
rotation-per-second="10deg"
src="./textured_mesh.glb/" disable-tap
environment-image="neutral"
camera-target="0m 0m 0m"
camera-orbit="0deg 90deg 12m"
orientation="0deg 0deg 0deg"
shadow-intensity=".9"
ar auto-rotate
camera-controls>
</model-viewer>
</div>
<div class="modelviewer-panel-button-container">
<div id="appearance-button" class="modelviewer-panel-button small checked" onclick="showTexture()">
Appearance
</div>
<div id="geometry-button" class="modelviewer-panel-button small" onclick="hideTexture()">Geometry</div>
</div>
</div>
</div>
<script>
let modelViewer;
let window_state = {
isModelLoaded: false,
isHidden: false,
savedMaterials: null,
savedExposure: null
};
document.addEventListener('DOMContentLoaded', () => {
modelViewer = document.getElementById('modelviewer');
// 等待模型加载完成
modelViewer.addEventListener('load', () => {
console.log('Model loaded, materials available:', modelViewer.model.materials.length);
window_state.isModelLoaded = true;
// 调试:打印材质信息
modelViewer.model.materials.forEach((mat, idx) => {
console.log(`Material ${idx}:`, {
name: mat.name,
hasBaseColor: !!mat.pbrMetallicRoughness.baseColorTexture,
hasMetallicRoughness: !!mat.pbrMetallicRoughness.metallicRoughnessTexture,
hasNormal: !!mat.normalTexture
});
});
});
modelViewer.addEventListener('error', (event) => {
console.error('Model loading error:', event);
});
});
function ensureModelLoaded() {
if (!window_state.isModelLoaded || !modelViewer.model || !modelViewer.model.materials) {
console.warn('Model not loaded yet');
return false;
}
return true;
}
function saveMaterialsState() {
if (!ensureModelLoaded()) return false;
console.log('Saving materials state...');
window_state.savedMaterials = [];
window_state.savedExposure = modelViewer.exposure;
for (let i = 0; i < modelViewer.model.materials.length; i++) {
const material = modelViewer.model.materials[i];
const pbr = material.pbrMetallicRoughness;
const materialState = {
baseColorTexture: null,
metallicRoughnessTexture: null,
normalTexture: null,
baseColorFactor: null,
metallicFactor: null,
roughnessFactor: null
};
// 保存纹理引用
try {
if (pbr.baseColorTexture && pbr.baseColorTexture.texture) {
materialState.baseColorTexture = pbr.baseColorTexture.texture;
console.log(`Saved baseColorTexture for material ${i}`);
}
if (pbr.metallicRoughnessTexture && pbr.metallicRoughnessTexture.texture) {
materialState.metallicRoughnessTexture = pbr.metallicRoughnessTexture.texture;
console.log(`Saved metallicRoughnessTexture for material ${i}`);
}
if (material.normalTexture && material.normalTexture.texture) {
materialState.normalTexture = material.normalTexture.texture;
console.log(`Saved normalTexture for material ${i}`);
}
// 保存材质参数
materialState.baseColorFactor = [...pbr.baseColorFactor];
materialState.metallicFactor = pbr.metallicFactor;
materialState.roughnessFactor = pbr.roughnessFactor;
} catch (error) {
console.error(`Error saving material ${i}:`, error);
}
window_state.savedMaterials.push(materialState);
}
console.log('Materials state saved:', window_state.savedMaterials);
return true;
}
function hideTexture() {
console.log('hideTexture called');
if (!ensureModelLoaded()) {
console.error('Cannot hide texture: model not loaded');
return;
}
let appearanceButton = document.getElementById('appearance-button');
let geometryButton = document.getElementById('geometry-button');
appearanceButton.classList.remove('checked');
geometryButton.classList.add('checked');
// 如果已经隐藏,直接返回
if (window_state.isHidden) return;
// 第一次隐藏时保存状态
if (!window_state.savedMaterials) {
if (!saveMaterialsState()) return;
}
// 隐藏所有纹理
try {
for (let i = 0; i < modelViewer.model.materials.length; i++) {
const material = modelViewer.model.materials[i];
const pbr = material.pbrMetallicRoughness;
if (pbr.baseColorTexture) {
pbr.baseColorTexture.setTexture(null);
}
if (pbr.metallicRoughnessTexture) {
pbr.metallicRoughnessTexture.setTexture(null);
}
if (material.normalTexture) {
material.normalTexture.setTexture(null);
}
}
window_state.isHidden = true;
modelViewer.environmentImage = '/static/env_maps/gradient.jpg';
modelViewer.exposure = 4;
console.log('Textures hidden successfully');
} catch (error) {
console.error('Error hiding textures:', error);
}
}
function showTexture() {
console.log('showTexture called');
if (!ensureModelLoaded()) {
console.error('Cannot show texture: model not loaded');
return;
}
let appearanceButton = document.getElementById('appearance-button');
let geometryButton = document.getElementById('geometry-button');
appearanceButton.classList.add('checked');
geometryButton.classList.remove('checked');
// 如果不在隐藏状态,直接返回
if (!window_state.isHidden) return;
// 如果没有保存的材质状态,无法恢复
if (!window_state.savedMaterials) {
console.warn('No saved materials to restore');
return;
}
// 恢复纹理
try {
for (let i = 0; i < modelViewer.model.materials.length && i < window_state.savedMaterials.length; i++) {
const material = modelViewer.model.materials[i];
const pbr = material.pbrMetallicRoughness;
const savedMaterial = window_state.savedMaterials[i];
// 恢复纹理
if (savedMaterial.baseColorTexture && pbr.baseColorTexture) {
pbr.baseColorTexture.setTexture(savedMaterial.baseColorTexture);
console.log(`Restored baseColorTexture for material ${i}`);
}
if (savedMaterial.metallicRoughnessTexture && pbr.metallicRoughnessTexture) {
pbr.metallicRoughnessTexture.setTexture(savedMaterial.metallicRoughnessTexture);
console.log(`Restored metallicRoughnessTexture for material ${i}`);
}
if (savedMaterial.normalTexture && material.normalTexture) {
material.normalTexture.setTexture(savedMaterial.normalTexture);
console.log(`Restored normalTexture for material ${i}`);
}
// 恢复材质参数
if (savedMaterial.baseColorFactor) {
pbr.setBaseColorFactor(savedMaterial.baseColorFactor);
}
if (typeof savedMaterial.metallicFactor === 'number') {
pbr.setMetallicFactor(savedMaterial.metallicFactor);
}
if (typeof savedMaterial.roughnessFactor === 'number') {
pbr.setRoughnessFactor(savedMaterial.roughnessFactor);
}
}
// 恢复环境设置
modelViewer.environmentImage = '/static/env_maps/white.jpg';
if (window_state.savedExposure !== undefined) {
modelViewer.exposure = window_state.savedExposure;
}
window_state.isHidden = false;
console.log('Textures restored successfully');
} catch (error) {
console.error('Error restoring textures:', error);
}
}
// 添加调试函数
function debugMaterials() {
if (!ensureModelLoaded()) return;
console.log('=== Current Materials Debug ===');
modelViewer.model.materials.forEach((mat, idx) => {
const pbr = mat.pbrMetallicRoughness;
console.log(`Material ${idx}:`, {
name: mat.name,
baseColorTexture: pbr.baseColorTexture?.texture || null,
metallicRoughnessTexture: pbr.metallicRoughnessTexture?.texture || null,
normalTexture: mat.normalTexture?.texture || null,
baseColorFactor: pbr.baseColorFactor,
metallicFactor: pbr.metallicFactor,
roughnessFactor: pbr.roughnessFactor
});
});
console.log('Window state:', window_state);
console.log('===============================');
}
// 暴露调试函数到全局
window.debugMaterials = debugMaterials;
</script>
</body>
</html>

View File

@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html>
<head>
<!-- Import the component -->
<script src="https://cdn.jsdelivr.net/npm/@google/model-viewer@3.1.1/dist/model-viewer.min.js" type="module"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const modelViewers = document.querySelectorAll('model-viewer');
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
modelViewers.forEach(modelViewer => {
modelViewer.setAttribute(
"environment-image",
"/static/env_maps/gradient.jpg"
);
// if (!isSafari) {
// modelViewer.setAttribute(
// "environment-image",
// "/static/env_maps/gradient.jpg"
// );
// } else {
// modelViewer.addEventListener('load', (event) => {
// const [material] = modelViewer.model.materials;
// let color = [43, 44, 46, 255];
// color = color.map(x => x / 255);
// material.pbrMetallicRoughness.setMetallicFactor(0.1); // 完全金属
// material.pbrMetallicRoughness.setRoughnessFactor(0.7); // 低粗糙度
// material.pbrMetallicRoughness.setBaseColorFactor(color); // CornflowerBlue in RGB
// });
// }
// modelViewer.addEventListener('load', (event) => {
// const [material] = modelViewer.model.materials;
// let color = [43, 44, 46, 255];
// color = color.map(x => x / 255);
// material.pbrMetallicRoughness.setMetallicFactor(0.1); // 完全金属
// material.pbrMetallicRoughness.setRoughnessFactor(0.7); // 低粗糙度
// material.pbrMetallicRoughness.setBaseColorFactor(color); // CornflowerBlue in RGB
// });
});
});
</script>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
}
.centered-container {
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
border-color: #e5e7eb;
border-style: solid;
border-width: 1px;
}
</style>
</head>
<body>
<div class="centered-container">
<div class="column is-mobile is-centered">
<model-viewer id="modelviewer" style="height: 640px; width: 500px;"
rotation-per-second="10deg"
src="./white_mesh.glb/" disable-tap
environment-image="neutral"
camera-target="0m 0m 0m"
camera-orbit="0deg 90deg 8m"
orientation="0deg 0deg 0deg"
shadow-intensity=".9"
ar auto-rotate
camera-controls>
</model-viewer>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
save_dir/env_maps/white.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
temp.glb Normal file

Binary file not shown.

BIN
test/images/114.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
test/images/chair.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

BIN
test/images/desk.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

BIN
test/images/desk1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB