Skip to content

Commit

Permalink
[Feature] Extend supported file types #1 (#27)
Browse files Browse the repository at this point in the history
* Added basic STL support

* Added AxesHelper

* Added basic FBX support

* Added basic OBJ support and fixed unloading models on prev/next

* Modified CHANGELOG

* Lint fix

* Update src/index.ts

Co-authored-by: Sawjan Gurung <[email protected]>

* Update src/App.vue

Co-authored-by: Sawjan Gurung <[email protected]>

* Update src/App.vue

Co-authored-by: Sawjan Gurung <[email protected]>

* Remove scene.remove for renderNewModel. no longer needed

---------

Co-authored-by: Alexander Stocker <[email protected]>
Co-authored-by: Sawjan Gurung <[email protected]>
  • Loading branch information
3 people authored Oct 14, 2024
1 parent 3a3b8fa commit cddef5f
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 24 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.idea
node_modules/
dist/
dist/
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
Added basic OBJ support
Added light and ambient
Added default material
Added basic FBX support
Added basic debugging mode
Added basic STL support

### Fixed
Fixed unloading models on prev/next

### Changed

Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

![3d model viewer ui](./docs/ss-light.png)

This is an extension for [ownCloud web](https://github.com/owncloud/web) for viewing 3D files. Currently, it can only display 3D models in `.glb` file format.
This is an extension for [ownCloud web](https://github.com/owncloud/web) for viewing 3D files.

## Feature Highlights ✨

- Supported formats:
- `.glb`
- Supported formats: [`.glb`, `.stl`, `.fbx`, `.obj`]
- Zoom/Rotate model
- Fullscreen view
- Navigate between model files
Expand Down Expand Up @@ -51,7 +50,7 @@ Now, you can access the app at https://localhost:9200

## 3D models

The app currently only supports 3D models in `.glb` format. You can find models on the following platforms:
You can find models on the following platforms:

- [sketchfab](https://sketchfab.com/)
- [3Dexport](https://3dexport.com/free-3d-models)
Expand Down
141 changes: 122 additions & 19 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,26 @@
<script setup lang="ts">
import { ref, unref, onMounted, onBeforeUnmount, computed } from 'vue'
import {
AmbientLight,
AxesHelper,
Scene,
Mesh,
PerspectiveCamera,
PointLight,
WebGLRenderer,
ACESFilmicToneMapping,
EquirectangularReflectionMapping,
Box3,
Vector3,
Euler,
TextureLoader
TextureLoader,
MeshPhongMaterial
} from 'three'
import WebGL from 'three/examples/jsm/capabilities/WebGL'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import {
AppLoadingSpinner,
Expand All @@ -75,7 +83,7 @@ import PreviewControls from './components/PreviewControls.vue'
import { id as appId } from '../public/manifest.json'
const environment = new URL('./assets/custom_light.jpg', import.meta.url).href
const supportExtensions = ['glb']
const supportExtensions = ['glb', 'stl', 'fbx', 'obj']
const router = useRouter()
const route = useRoute()
Expand All @@ -92,6 +100,7 @@ let iniCamPosition: Vector3 | null = null
let iniCamZPosition: number = 0
const iniCamRotation: Euler = new Euler(0, 0, 0)
const animTimeoutSec = 1
const debugIsEnabled = false
// =====================
// props
Expand Down Expand Up @@ -137,13 +146,14 @@ onMounted(async () => {
// camera controls
controls = new OrbitControls(camera, renderer.domElement)
controls.minDistance = 1
controls.minDistance = 0
controls.maxDistance = 100
// load environment texture
try {
await loadEnvironment()
await renderModel()
await renderModel(unref(fileType))
loadLights()
} catch (e) {
cleanup3dScene()
hasError.value = true
Expand Down Expand Up @@ -182,6 +192,7 @@ const modelFiles = computed<Resource[]>(() => {
return sortHelper(files, [{ name: unref(sortBy) }], unref(sortBy), unref(sortDir))
})
const activeModelFile = computed(() => unref(modelFiles)[unref(activeIndex)])
const fileType = computed(() => unref(activeModelFile)?.extension)
// =====================
// methods
Expand All @@ -192,22 +203,65 @@ async function updateUrl() {
unref(activeModelFile)
)
}
async function loadEnvironment() {
const texture = await new TextureLoader().loadAsync(environment)
texture.mapping = EquirectangularReflectionMapping
scene.environment = texture
}
async function renderModel() {
const model = await new GLTFLoader().loadAsync(unref(currentUrl), (xhr) => {
const LoaderMap = {
glb: GLTFLoader,
stl: STLLoader,
fbx: FBXLoader,
obj: OBJLoader
}
const materialParams = {
transparent: true,
opacity: 0.8,
color: 0xd7d7d7,
flatShading: true
}
const lightParams = {
color: 0xffffff,
intensity: 1000,
posX: 2.5,
posY: 15,
posZ: 25,
ambient: true
}
async function renderModel(extension: string) {
const ModelLoader = LoaderMap[extension]
const model = await new ModelLoader().loadAsync(unref(currentUrl), (xhr) => {
const downloaded = Math.floor((xhr.loaded / xhr.total) * 100)
if (downloaded % 5 === 0) {
loadingProgress.value = downloaded
}
})
const modelScene = model.scene
// model size
const box = new Box3().setFromObject(modelScene)
debug(model)
const box = new Box3()
if (!model.hasOwnProperty('scene') && extension === 'stl') {
const mesh = new Mesh(model, defaultMaterial())
scene.add(mesh)
box.setFromBufferAttribute(model.attributes.position)
} else if (!model.hasOwnProperty('scene') && (extension === 'fbx' || extension === 'obj')) {
box.setFromObject(model)
model.traverse(function (child) {
if (child.isMesh) {
child.material = defaultMaterial()
}
})
scene.add(model)
} else {
box.setFromObject(model.scene)
}
iniCamPosition = box.getCenter(new Vector3())
// direct camera at model
Expand All @@ -216,15 +270,36 @@ async function renderModel() {
camera.position.z = iniCamZPosition
camera.lookAt(iniCamPosition)
// center model
modelScene.position.sub(iniCamPosition)
scene.add(modelScene)
loadingModel.value = false
currentModel.value = modelScene
if (extension === 'glb') {
const modelScene = model.scene
// center model
modelScene.position.sub(iniCamPosition)
scene.add(modelScene)
currentModel.value = modelScene
} else {
currentModel.value = scene
}
unref(sceneWrapper).appendChild(renderer.domElement)
render(Date.now())
}
function loadLights(): void {
const light = new PointLight(lightParams.color, lightParams.intensity)
light.position.set(lightParams.posX, lightParams.posY, lightParams.posZ)
scene.add(light)
if (lightParams.ambient) {
const ambientLight = new AmbientLight()
scene.add(ambientLight)
}
}
function defaultMaterial(): MeshPhongMaterial {
return new MeshPhongMaterial(materialParams)
}
function render(animStartTime: number) {
animationId.value = requestAnimationFrame(() => render(animStartTime))
// TODO: enable animation
Expand All @@ -236,29 +311,29 @@ function render(animStartTime: number) {
controls.update()
renderer.render(scene, camera)
}
async function renderNewModel() {
cancelAnimationFrame(unref(animationId))
scene.remove(scene.getObjectByName(unref(currentModel).name))
await updateUrl()
loadingModel.value = true
hasError.value = false
await renderModel()
await renderModel(unref(fileType))
}
function cleanup3dScene() {
scene.traverse((obj) => {
scene.remove(obj)
})
cancelAnimationFrame(unref(animationId))
renderer.dispose()
}
function changeCursor(state: string) {
const el = unref(sceneWrapper)
if (el.classList.contains('model-viewport')) {
el.style.cursor = state
}
}
async function setActiveModel(driveAliasAndItem: string) {
for (let i = 0; i < unref(modelFiles).length; i++) {
if (
Expand All @@ -271,6 +346,7 @@ async function setActiveModel(driveAliasAndItem: string) {
}
}
}
function updateLocalHistory() {
if (!unref(currentFileContext)) {
return
Expand All @@ -286,38 +362,54 @@ function updateLocalHistory() {
query: { ...unref(route).query, ...query }
})
}
async function next() {
if (!unref(isModelReady)) {
return
}
if (unref(activeIndex) + 1 >= unref(modelFiles).length) {
activeIndex.value = 0
} else {
activeIndex.value++
}
updateLocalHistory()
await unloadModels()
// TODO: how to prevent activeFiles from being reduced
// load activeFiles
await loadFolderForFileContext(unref(currentFileContext))
await renderNewModel()
}
async function prev() {
if (!unref(isModelReady)) {
return
}
if (unref(activeIndex) === 0) {
activeIndex.value = unref(modelFiles).length - 1
} else {
activeIndex.value--
}
updateLocalHistory()
await unloadModels()
// TODO: how to prevent activeFiles from being reduced
// load activeFiles
await loadFolderForFileContext(unref(currentFileContext))
await renderNewModel()
}
async function unloadModels(): Promise<void> {
for (let i = scene.children.length - 1; i >= 0; i--) {
let obj = scene.children[i]
if (unref(obj.type) === 'Group' || unref(obj.type) === 'Mesh') {
scene.remove(obj)
}
}
}
function toggleFullscreenMode() {
const activateFullscreen = !unref(isFullScreenModeActivated)
const el = unref(sceneWrapper)
Expand All @@ -332,6 +424,7 @@ function toggleFullscreenMode() {
}
}
}
function resetModelPosition() {
if (unref(isModelReady)) {
camera.position.copy(iniCamPosition)
Expand All @@ -340,6 +433,15 @@ function resetModelPosition() {
camera.lookAt(iniCamPosition)
}
}
function debug(output) {
if (debugIsEnabled) {
scene.add(new AxesHelper(10))
console.log('####### DEBUG 3D MODEL #######')
console.log(output)
console.log('#####################')
}
}
</script>

<style lang="scss" scoped>
Expand All @@ -352,6 +454,7 @@ function resetModelPosition() {
cursor: grab;
}
}
#spinner {
& > div {
width: unset;
Expand Down
12 changes: 12 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ export default defineWebApplication({
{
extension: 'glb',
label: 'View 3D Model'
},
{
extension: 'stl',
label: 'View 3D Model'
},
{
extension: 'fbx',
label: 'View 3D Model'
},
{
extension: 'obj',
label: 'View 3D Model'
}
]
},
Expand Down

0 comments on commit cddef5f

Please sign in to comment.