The depth of the children of object continues.

“three”: “^0.167.1”
This is the sceneJSON structure after const sceneJSON = scene.toJSON();

Other columns are normal, but the depth of the children of object continues.

The following is the scene creation code, and it is being used as a global object in window.
Please help.

1 Like

That looks normal to me?

Is there a problem with it?

When JSON.stringify(sceneJSON) is called, Maximum call exceeded occurs due to depth or recursion of scene.object.children.

Thank manthrax for answer,

1 Like

i don’t think you can do that in javascript.

const a = {}
a.b = a
JSON.stringify(a) // ❌

threejs objs are the same, they link back to their parents. you need to use a qualified exporter, not the scene tree.

Hi, Dan.

sceneJSON is generated by scene.toJSON(), which is a three.js object.
Therefore, children are also automatically generated.

Please take a good look at the image above

 try {
       const sceneJSON = scene.toJSON();
       const sceneString = JSON.stringify(sceneJSON)
       const blob = new Blob([sceneString], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = url;
        link.download = _getFilename('.json');
        link.click();
        URL.revokeObjectURL(url);
 } catch (err) {
        console.log(err);
    }

Thank you

1 Like

You don’t need the second line, scene.toJSON() already returns a JSON string.

1 Like

The result is

"[object Object]"

My scene code is SimCity clone.

import * as THREE from 'three';
import cityWowApp from './cityWowApp.js';
import { AssetMgr } from './assets/assetMgr.js';
import { CameraManager } from './camera.js';
import { InputManager } from './input.js';
import { City } from './wow/city.js';
import { WowObject } from './wow/wowObject.js';
import { exportImage, saveScene, openScene } from './util/index.js';

export class CityMgr {
  city;
 
  focusedObject = null;
 
  inputManager;
 
  selectedObject = null;

  constructor() {
    this.wowContainer = cityWowApp.cityUI.wowContainer
   
    this.renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, preserveDrawingBuffer: true }); // preserveDrawingBuffer: true -> 이미지 저장시 배경색 포함
    this.renderer.setSize(this.wowContainer.clientWidth, this.wowContainer.clientHeight); // 렌더러 크기
    this.renderer.setPixelRatio(window.devicePixelRatio); // 픽셀 비율
    this.renderer.setClearColor(0x555555, 0.2); // 배경색
    this.renderer.shadowMap.enabled = true; // 그림자
    this.renderer.shadowMap.type = THREE.PCFShadowMap; // 그림자 유형

    // scene 구성
    this.scene = new THREE.Scene();
    this.inputManager = new InputManager(this.wowContainer); // 사용자 입력 관리
    this.cameraManager = new CameraManager(this.wowContainer); // 카메라 관리

    this.wowContainer.appendChild(this.renderer.domElement);

    // 객체 선택을 위한 변수
    this.raycaster = new THREE.Raycaster(); // 레이캐스터

    cityWowApp.assetMgr = new AssetMgr(() => { // 자산 관리자(모든 모델 로드 완료 후 실행)
      cityWowApp.cityUI.showLoadingText(false); // 로딩 텍스트 숨김
      this.onClearScene(); // 초기화
    });

    window.addEventListener('resize', this.#onResize.bind(this), {passive: true}); // 창 크기 조정 이벤트
  }

  #initialize(city) {
    this.scene.clear(); // 이전 scene 삭제
    this.scene.add(city); // 시티 객체 추가
    this.#setupLights(); // 조명 설정
    this.#setupGrid(city); // 격자 설정
  }

  #setupGrid(city) {
    const gridMaterial = new THREE.MeshBasicMaterial({ // 격자 머티리얼
      color: 0x00ff00,
      map: cityWowApp.assetMgr.textures['grid'],
      transparent: true,
      opacity: 0.3
    });
    const size = city.size
    gridMaterial.map.repeat = new THREE.Vector2(size, size); // 격자 크기
    gridMaterial.map.wrapS = THREE.RepeatWrapping; // 텍스처가 수평으로 래핑되는 방식을 정의하며 UV 매핑의 U에 해당
    gridMaterial.map.wrapT = THREE.RepeatWrapping; // 텍스처가 수직으로 래핑되는 방식을 정의하며 UV 매핑의 V에 해당

    const grid = new THREE.Mesh( // 격자 메시
      new THREE.BoxGeometry(size, 0.1, size), // 박스 지오메트리
      gridMaterial
    );
    grid.position.set(size / 2 - 0.5, -0.04, size / 2 - 0.5); // 격자 위치
    this.scene.add(grid);
  }

  #setupLights() {
    const light = new THREE.DirectionalLight(0xffffff, 2) // 방향성 빛
    light.position.set(-10, 20, 0); // 빛 위치
    light.shadow.camera.left = -20;
    light.shadow.camera.right = 20;
    light.shadow.camera.top = 20;
    light.shadow.camera.bottom = -20;
    light.shadow.camera.near = 10;
    light.shadow.camera.far = 50;

    light.shadow.mapSize.width = 2048;
    light.shadow.mapSize.height = 2048;

    light.shadow.normalBias = 0.01;
    light.castShadow = true;
    
    this.scene.add(light);
    this.scene.add(new THREE.AmbientLight(0xffffff, 1)); // 주변 빛
  }
  
  #start() {
    this.renderer.setAnimationLoop(this.#draw.bind(this)); // 애니메이션 시작
  }

  #draw() {
    this.city.draw();
    this.updateFocusedObject(); // 마우스 포커스된 객체 업데이트

    if (this.inputManager.isLeftMouseDown) {
      this.onUseToolbar(); // 툴바 사용
    }

    this.renderer.render(this.scene, this.cameraManager.camera); // 렌더링
  }

  #simulate() {
    if (cityWowApp.cityUI.isPaused) return;

    // Update the city data model first, then update the scene
    this.city.simulate(1);

    cityWowApp.cityUI.updateTitleBar(this);
    cityWowApp.cityUI.updateInfoPanel(this.selectedObject);
  }

  
  #raycast() {
    const _El = this.renderer.domElement
    const coords = {
      x: (this.inputManager.mouse.x / _El.clientWidth) * 2 - 1,
      y: -(this.inputManager.mouse.y / _El.clientHeight) * 2 + 1
    };

    this.raycaster.setFromCamera(coords, this.cameraManager.camera);

    const intersections = this.raycaster.intersectObjects(this.city.root.children, true); // 레이캐스팅
    // The WowObject attached to the mesh is stored in the user data
    return intersections.length > 0 ? intersections[0].object.userData : null; // 첫 번째 객체 반환
  }
  
  #onResize() {
    this.cameraManager.resize(this.wowContainer);
    this.renderer.setSize(this.wowContainer.clientWidth, this.wowContainer.clientHeight);
  }
  
  stop() {
    this.renderer.setAnimationLoop(null);
  }

  onClearScene() {
    this.city = new City(16);
    this.#initialize(this.city);
    this.#start();

    if (this.intervalId) {
      clearInterval(this.intervalId);
    }
    this.intervalId = setInterval(this.#simulate.bind(this), 1000);
  }

//   onClearScene() {
//     this.city = new City(16);
//     this.#initialize(this.city);
//     this.#start();

//     if (this.intervalId) {
//       cancelAnimationFrame(this.intervalId);
//     }
//     this.#animate();
// }

//   #animate() {
//     this.#simulate();
//     this.intervalId = requestAnimationFrame(this.#animate.bind(this));
//   }

  /**
   * Sets the currently selected object and highlights it
   */
  updateSelectedObject() {
    this.selectedObject?.setSelected(false);
    this.selectedObject = this.focusedObject;
    this.selectedObject?.setSelected(true);
  }

  /**
   * Sets the object that is currently highlighted
   */
  updateFocusedObject() {  
    this.focusedObject?.setFocused(false); // 이전 포커스된 객체 해제
    const newObject = this.#raycast(); // 새로운 포커스된 객체
    if (newObject !== this.focusedObject) { // 새로운 객체가 이전 객체와 다르면
      this.focusedObject = newObject;
    }
    this.focusedObject?.setFocused(true); // 포커스된 객체 설정
  }

  onUseToolbar() {
    switch (cityWowApp.cityUI.activeType) {
      case 'select':
        this.updateSelectedObject();
        cityWowApp.cityUI.updateInfoPanel(this.selectedObject);
        break;
      case 'eraser':
        if (this.focusedObject) {
          const { x, y } = this.focusedObject;
          this.city.eraser(x, y);
        }        
        break;
      default:
        if (this.focusedObject) {
          const { x, y } = this.focusedObject;
          this.city.placeBuilding(x, y, cityWowApp.cityUI.activeType);
        }
        break;
    }
  }

  onSaveModel() {   
    saveScene(this.scene);
  }

  onOpenModel() {
    openScene((loadedScene) => {
        this.scene = loadedScene; 
        this.#draw();
    });
  }

  onExportImg() {        
    exportImage(this.renderer)    
  }
}
1 Like

@Greh maybe try structuredClone() to see if that makes any difference:

  const sceneString = JSON.stringify( structuredClone( sceneJSON ) )
DOMException: Failed to execute 'structuredClone' on 'Window': function onRotationChange() {
      quaternion.setFromEuler(rotation, false);
    } could not be cloned.
    at _saveSceneFile (http://127.0.0.1:3000/js/util/index.js?t=1722985500415:235:45)
    at saveScene (http://127.0.0.1:3000/js/util/index.js?t=1722985500415:227:37)
    at CityMgr.onSaveModel (http://127.0.0.1:3000/js/cityMgr.js?t=1722985500415:236:5)
    at CityUI.onToolSelected (http://127.0.0.1:3000/js/cityUI.js?t=1722985500415:55:26)
    at HTMLButtonElement.<anonymous> (http://127.0.0.1:3000/js/util/index.js?t=1722985500415:117:47)

I think it’s quite possible you’re just overloading threejs toJSON which uses naive recursion to generate json output. Or its possible there is some circular reference in there that is tripping it up, but generally the classes are designed to prevent that.

More generally/ironically… the json format isn’t really great for serialization. If you’re trying to save the scene you can use the GLTFExporter to save it as gltf/glb. If you’re trying to serialize your app state, you might be better off space and time wise by serializing the game datastructures themselves, independent of the scene graph.

May I ask what you need the json format of the scene for?

Thank you very much for your detailed answer.

I am a beginner in Three.js.
I tried with a little help from gpt, just a little bit beyond the follow-through level, but I got the same answer, which is disappointing.
Rather than a special strategy for me, I would appreciate it if you could give me a simple sample like you mentioned.
I want to save and open it so that the user can do additional work.
I have already written the code using pako because of the file size.

The code below also failed.


function _saveSceneFile(scene) {
    const exporter = new GLTFExporter();
    debugger
    // Remove circular references from userData
    scene.traverse((object) => {
        if (object.userData) {
            removeCircularReferences(object.userData);
        }
    });

    exporter.parse(scene, (result) => {
        const output = JSON.stringify(result, null, 2);
        saveString(output, filename);
    }, { binary: false });
}
// Helper function to remove circular references from userData
function removeCircularReferences(obj, seen = new WeakSet()) {
    if (obj && typeof obj === 'object') {
        if (seen.has(obj)) {
            return;
        }
        seen.add(obj);
        for (const key in obj) {
            if (obj.hasOwnProperty(key)) {
                if (typeof obj[key] === 'object') {
                    removeCircularReferences(obj[key], seen);
                }
            }
        }
    }
}
// Helper function to save a string as a file
function saveString(text) {
    try {  
        const blob = new Blob([text], { type: 'text/plain' });
        const link = document.createElement('a');
        link.href = URL.createObjectURL(blob);
        link.download = _getFilename('.json');
        link.click();
        URL.revokeObjectURL(url); // URL 해제
    } catch (err) {
        console.log(err);
    }
}

I want to provide the user with a format that can save and open files only in Three.js.
Please help me.

Thank you so much.

Ohh that’s interesting… I see in the code you posted, about removing circular references?
That is interesting, because it’s possible that that is what caused your stack overflow when using toJSON()?
.userData is a field on object3ds that is meant for holding your own custom data, and will attempt to get serialized by toJSON… and if it contains complex data or circular references… that might explain the original error.

It might be worth doing something like:

scene.traverse(e=>e.userData={});
scene.toJSON()

And see if the error goes away?
If so… then maybe you can continue with toJSON()

If you use the GLTFExporter, you will get a gltf file out, that can be loaded in other apps like blender or viewed in online viewers, but won’t necessarily be able to load back into you game without a bunch more work.

structuredClone() was kind of supposed to resolve circular references as well but got stuck on that onRotationChange() function.

It seems that scene.toJSON() is processing everything without an issue but something in that scene is just not right.

What @manthrax suggested might even help.

Thanks for the support.
But all of them are giving me errors.
Is there a better way?

Thanks for the support.
But all of them are giving me errors.
Is there a better way?

Thank you so much