mirror of
https://codeberg.org/icewind/vbsp-to-gltf.git
synced 2026-06-03 10:14:08 +02:00
serve viewer
This commit is contained in:
parent
bfe990521f
commit
da0ef79fa1
10 changed files with 126 additions and 96 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -9,3 +9,4 @@ result
|
||||||
cache
|
cache
|
||||||
config.toml
|
config.toml
|
||||||
node_modules
|
node_modules
|
||||||
|
dist
|
||||||
20
Cargo.lock
generated
20
Cargo.lock
generated
|
|
@ -1040,6 +1040,25 @@ version = "1.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126"
|
checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "include_dir"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
|
||||||
|
dependencies = [
|
||||||
|
"include_dir_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "include_dir_macros"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.4.0"
|
version = "2.4.0"
|
||||||
|
|
@ -2924,6 +2943,7 @@ dependencies = [
|
||||||
"gltf-json",
|
"gltf-json",
|
||||||
"http",
|
"http",
|
||||||
"image",
|
"image",
|
||||||
|
"include_dir",
|
||||||
"miette",
|
"miette",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,10 @@ reqwest = { version = "0.12.7", optional = true, default-features = false, featu
|
||||||
async-tempfile = { version = "0.6.0", optional = true }
|
async-tempfile = { version = "0.6.0", optional = true }
|
||||||
tower-http = { version = "0.5.2", optional = true, features = ["cors"] }
|
tower-http = { version = "0.5.2", optional = true, features = ["cors"] }
|
||||||
http = { version = "1.1.0", optional = true }
|
http = { version = "1.1.0", optional = true }
|
||||||
|
include_dir = { version = "0.7.4", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
server = ["url", "toml", "axum", "tokio", "reqwest", "async-tempfile", "tower-http", "http"]
|
server = ["url", "toml", "axum", "tokio", "reqwest", "async-tempfile", "tower-http", "http", "include_dir"]
|
||||||
|
|
||||||
[profile.dev.package."*"]
|
[profile.dev.package."*"]
|
||||||
opt-level = 2
|
opt-level = 2
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ use axum::response::{Html, IntoResponse, Response};
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use http::Method;
|
use http::header::{CACHE_CONTROL, CONTENT_TYPE, ETAG};
|
||||||
|
use http::{HeaderValue, Method};
|
||||||
|
use include_dir::{include_dir, Dir};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::fs::{read, read_to_string, write};
|
use std::fs::{read, read_to_string, write};
|
||||||
|
|
@ -144,6 +146,9 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/gltf/:map", get(convert))
|
.route("/gltf/:map", get(convert))
|
||||||
|
.route("/view/:map", get(view))
|
||||||
|
.route("/assets/:asset", get(viewer_asset))
|
||||||
|
.route("/transcoders/:asset", get(transcoder_asset))
|
||||||
.route("/", get(index))
|
.route("/", get(index))
|
||||||
.layer(cors)
|
.layer(cors)
|
||||||
.with_state(Arc::new(app));
|
.with_state(Arc::new(app));
|
||||||
|
|
@ -218,6 +223,87 @@ async fn index() -> impl IntoResponse {
|
||||||
Html(include_str!("./index.html"))
|
Html(include_str!("./index.html"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn view() -> impl IntoResponse {
|
||||||
|
Html(include_str!("./viewer/dist/index.html"))
|
||||||
|
}
|
||||||
|
|
||||||
|
static VIEWER_ASSETS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/src/server/viewer/dist/assets");
|
||||||
|
|
||||||
|
pub async fn viewer_asset(asset: Path<String>) -> impl IntoResponse {
|
||||||
|
let path = asset.as_str();
|
||||||
|
match VIEWER_ASSETS.get_file(path) {
|
||||||
|
Some(file) => (
|
||||||
|
[
|
||||||
|
(
|
||||||
|
CONTENT_TYPE,
|
||||||
|
HeaderValue::from_str(guess_mime(path)).unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ETAG,
|
||||||
|
HeaderValue::from_static("these_should_be_fully_static"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
CACHE_CONTROL,
|
||||||
|
HeaderValue::from_static("public, max-age=2592000, immutable"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
file.contents(),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
None => StatusCode::NOT_FOUND.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static TRANSCODER_ASSETS: Dir<'_> =
|
||||||
|
include_dir!("$CARGO_MANIFEST_DIR/src/server/viewer/transcoders");
|
||||||
|
|
||||||
|
pub async fn transcoder_asset(asset: Path<String>) -> impl IntoResponse {
|
||||||
|
let path = asset.as_str();
|
||||||
|
match TRANSCODER_ASSETS.get_file(path) {
|
||||||
|
Some(file) => (
|
||||||
|
[
|
||||||
|
(
|
||||||
|
CONTENT_TYPE,
|
||||||
|
HeaderValue::from_str(guess_mime(path)).unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ETAG,
|
||||||
|
HeaderValue::from_static("these_should_be_fully_static"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
CACHE_CONTROL,
|
||||||
|
HeaderValue::from_static("public, max-age=2592000, immutable"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
file.contents(),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
None => StatusCode::NOT_FOUND.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn guess_mime(path: &str) -> &'static str {
|
||||||
|
if path.ends_with("svg") {
|
||||||
|
"image/svg+xml"
|
||||||
|
} else if path.ends_with("png") {
|
||||||
|
"image/png"
|
||||||
|
} else if path.ends_with("webp") {
|
||||||
|
"image/webp"
|
||||||
|
} else if path.ends_with("css") {
|
||||||
|
"text/css"
|
||||||
|
} else if path.ends_with("wasm") {
|
||||||
|
"application/wasm"
|
||||||
|
} else if path.ends_with("js")
|
||||||
|
|| path.ends_with("ts")
|
||||||
|
|| path.ends_with("jsx")
|
||||||
|
|| path.ends_with("tsx")
|
||||||
|
{
|
||||||
|
"text/javascript"
|
||||||
|
} else {
|
||||||
|
"text/plain"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn convert(
|
async fn convert(
|
||||||
State(app): State<Arc<App>>,
|
State(app): State<Arc<App>>,
|
||||||
Path(map): Path<String>,
|
Path(map): Path<String>,
|
||||||
|
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
import * as THREE from 'three'
|
|
||||||
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls'
|
|
||||||
import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader'
|
|
||||||
import Stats from 'three/examples/jsm/libs/stats.module'
|
|
||||||
|
|
||||||
const scene = new THREE.Scene()
|
|
||||||
scene.add(new THREE.AxesHelper(5))
|
|
||||||
|
|
||||||
const light = new THREE.SpotLight(0xffffff, Math.PI * 20)
|
|
||||||
light.position.set(5, 5, 5)
|
|
||||||
scene.add(light)
|
|
||||||
|
|
||||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
|
|
||||||
camera.position.z = 2
|
|
||||||
|
|
||||||
const renderer = new THREE.WebGLRenderer()
|
|
||||||
// Since Three r150, lighting has changed significantly with every version up to three r158
|
|
||||||
// keep the threejs defaults, and reduce light watts in blender if using punctual lights
|
|
||||||
// if using Threejs lights, then you need to experiment with light intensity.
|
|
||||||
// renderer.physicallyCorrectLights = true //deprecated
|
|
||||||
// renderer.useLegacyLights = false //deprecated
|
|
||||||
renderer.shadowMap.enabled = true
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
|
||||||
document.body.appendChild(renderer.domElement)
|
|
||||||
|
|
||||||
const controls = new OrbitControls(camera, renderer.domElement)
|
|
||||||
controls.enableDamping = true
|
|
||||||
|
|
||||||
const loader = new GLTFLoader()
|
|
||||||
loader.load(
|
|
||||||
'models/monkey.glb',
|
|
||||||
function (gltf) {
|
|
||||||
// gltf.scene.traverse(function (child) {
|
|
||||||
// if ((child as THREE.Mesh).isMesh) {
|
|
||||||
// const m = (child as THREE.Mesh)
|
|
||||||
// m.receiveShadow = true
|
|
||||||
// m.castShadow = true
|
|
||||||
// }
|
|
||||||
// if (((child as THREE.Light)).isLight) {
|
|
||||||
// const l = (child as THREE.SpotLight)
|
|
||||||
// l.castShadow = true
|
|
||||||
// l.shadow.bias = -.003
|
|
||||||
// l.shadow.mapSize.width = 2048
|
|
||||||
// l.shadow.mapSize.height = 2048
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
scene.add(gltf.scene)
|
|
||||||
},
|
|
||||||
(xhr) => {
|
|
||||||
console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
window.addEventListener('resize', onWindowResize, false)
|
|
||||||
|
|
||||||
function onWindowResize() {
|
|
||||||
camera.aspect = window.innerWidth / window.innerHeight
|
|
||||||
camera.updateProjectionMatrix()
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
|
||||||
render()
|
|
||||||
}
|
|
||||||
|
|
||||||
const stats = new Stats()
|
|
||||||
document.body.appendChild(stats.dom)
|
|
||||||
|
|
||||||
function animate() {
|
|
||||||
requestAnimationFrame(animate)
|
|
||||||
|
|
||||||
controls.update()
|
|
||||||
|
|
||||||
render()
|
|
||||||
|
|
||||||
stats.update()
|
|
||||||
}
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
renderer.render(scene, camera)
|
|
||||||
}
|
|
||||||
|
|
||||||
animate()
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>Three.js TypeScript Tutorials by Sean Bradley : https://sbcode.net/threejs</title>
|
<title>TF2 Map viewer</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="loading">
|
<body class="loading">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import './style.css'
|
import './style.css'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import {PointerLockControls} from 'three/addons/controls/PointerLockControls.js'
|
import {PointerLockControls} from 'three/addons/controls/PointerLockControls.js'
|
||||||
// import {OrbitControls} from 'three/addons/controls/OrbitControls.js'
|
|
||||||
import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js'
|
import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js'
|
||||||
import Stats from 'three/addons/libs/stats.module.js'
|
import Stats from 'three/addons/libs/stats.module.js'
|
||||||
import {KTX2Loader} from 'three/addons/loaders/KTX2Loader.js';
|
import {KTX2Loader} from 'three/addons/loaders/KTX2Loader.js';
|
||||||
|
|
@ -17,7 +16,7 @@ const renderer = new THREE.WebGLRenderer({antialias: true})
|
||||||
const loader = new GLTFLoader();
|
const loader = new GLTFLoader();
|
||||||
|
|
||||||
const ktx2Loader = new KTX2Loader();
|
const ktx2Loader = new KTX2Loader();
|
||||||
ktx2Loader.setTranscoderPath('transcoders/basis/');
|
ktx2Loader.setTranscoderPath('/transcoders/');
|
||||||
ktx2Loader.detectSupport(renderer);
|
ktx2Loader.detectSupport(renderer);
|
||||||
|
|
||||||
loader.setKTX2Loader(ktx2Loader);
|
loader.setKTX2Loader(ktx2Loader);
|
||||||
|
|
@ -56,15 +55,23 @@ const dirLight = new THREE.DirectionalLight(0xefefff, 1.5);
|
||||||
dirLight.position.set(10, 10, 10);
|
dirLight.position.set(10, 10, 10);
|
||||||
scene.add(dirLight);
|
scene.add(dirLight);
|
||||||
|
|
||||||
|
let map;
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const map = urlParams.get('map');
|
if (window.location.pathname.startsWith('/view/')) {
|
||||||
|
map = window.location.pathname.substring('/view/'.length);
|
||||||
|
} else {
|
||||||
|
map = urlParams.get('map');
|
||||||
|
}
|
||||||
|
const textureScale = urlParams.get('texture_scale') || 0.25;
|
||||||
|
const textures = urlParams.get('textures') || true;
|
||||||
console.log(map);
|
console.log(map);
|
||||||
|
|
||||||
loader.load(`https://gltf.demos.tf/gltf/${map}.glb?texture_scale=0.25`, (gltf) => {
|
loader.load(`/gltf/${map}.glb?texture_scale=${textureScale}&textures=${textures}`, (gltf) => {
|
||||||
document.body.classList.remove('loading');
|
document.body.classList.remove('loading');
|
||||||
gltf.scene.traverse(child => {
|
gltf.scene.traverse(child => {
|
||||||
if (child.material) {
|
if ((child as THREE.Mesh).material) {
|
||||||
child.material.metalness = 0;
|
((child as THREE.Mesh).material as any as THREE.MaterialJSON).metalness = 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
scene.add(gltf.scene)
|
scene.add(gltf.scene)
|
||||||
|
|
@ -88,7 +95,7 @@ let movementScale = 250;
|
||||||
|
|
||||||
const _vector = new THREE.Vector3();
|
const _vector = new THREE.Vector3();
|
||||||
|
|
||||||
function moveForward(distance) {
|
function moveForward(distance: number): void {
|
||||||
_vector.copy(controls.getDirection(_vector));
|
_vector.copy(controls.getDirection(_vector));
|
||||||
|
|
||||||
camera.position.addScaledVector(_vector, distance);
|
camera.position.addScaledVector(_vector, distance);
|
||||||
|
|
@ -125,8 +132,6 @@ function animate() {
|
||||||
movementScale = 250;
|
movementScale = 250;
|
||||||
}
|
}
|
||||||
|
|
||||||
controls.update()
|
|
||||||
|
|
||||||
renderer.render(scene, camera)
|
renderer.render(scene, camera)
|
||||||
|
|
||||||
stats.update()
|
stats.update()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue