Three.js를 사용하여 의자의 3D 모델에 대한 다양한 사용자 지정 색상을 입히는 방법을 알아 봅니다.

출처 : https://tympanus.net/codrops/2019/09/17/how-to-build-a-color-customizer-app-for-a-3d-model-with-three-js/ 

 

 

 

이 자습서에서는 Three.js를 사용하여 의자의 3D 모델의 색상을 변경할 수 있는 사용자 지정자 앱을 만드는 방법을 배웁니다.

 

 

 

빠른 소개 

 

이 도구는 Vans shoe 사용자 정의 프로그램에서 영감을 얻어 제작되었으며 놀라운 JavaScript 3D 라이브러리 Three.js를 사용합니다.

 

이 자습서에서는 JavaScript, HTML 및 CSS에 익숙하다고 가정하겠습니다.

 

나는 실제로 당신을 가르치기 위해 여기에서 조금 다른 것을 할 것입니다.이 튜토리얼과 관련이 없는 부분을 복사 / 붙여 넣기 하지 마십시오. 우리는 모든 CSS로 시작할 것입니다 그 자리에. CSS는 실제로 앱 주변의 드레싱만을 위한 것이며 UI에만 초점을 맞추고 있습니다. 즉, HTML을 붙여 넣을 때마다 CSS의 기능을 빠르게 설명하겠습니다. 시작하자.

 

 

1 부 : 3D 모델 

 

이 부분을 완전히 건너 뛰고 싶다면 자유롭게 사용하십시오. 그러나 모든 부분이 어떻게 작동하는지에 대해 더 깊이 이해하기 위해 읽어야 할 수도 있습니다. 

 

이것은 3D 모델링 튜토리얼은 아니지만 블렌더에서 모델을 설정하는 방법을 설명하고, 직접 무언가를 만들려면 온라인 어딘가에서 찾은 무료 모델을 변경하거나 다른 사람에게 지시하십시오. 시운전. 다음은 Chairs 3D 모델의 제작 방법에 대한 정보입니다.

 

이 자습서의 3D 모델은 JavaScript에 호스팅 되어 포함되어 있으므로 블렌더 사용에 대해 더 자세히 보고 자신 만의 모델을 만드는 방법을 배우지 않는 한 다운로드 하거나 이 작업을 수행 할 필요가 없습니다.

 

Scale 

 

규모는 대략 실제 세계와 비슷합니다. 이것이 중요한지 모르겠지만 옳은 일이라고 생각합니다. 왜 안 되겠습니까?

 

 

 

레이어링 및 명명 규칙 

 

이 부분은 중요합니다. 독립적으로 사용자 정의하려는 객체의 각 요소는 3D 장면에서 고유 한 객체 여야 하며 각 항목의 이름은 고유해야 합니다. 여기에는 등받이, 받침대, 쿠션, 다리 및 지지대가 있습니다. 모두 support라고 하는 3 개의 항목을 말하면 Blender는 해당 이름을 support, supports.001, supports.002로 지정합니다. JavaScript에서는 "includes ("supports ")"를 사용하여 문자열이 포함 된 모든 개체를 지원하기 때문에 지원합니다.

 

 

 

Placement 

 

모델은 발이 바닥에 있는 세계 원점에 배치해야 합니다. 이상적으로 올바른 방향을 향해야 하지만 JavaScript를 통해 쉽게 회전 할 수 있으며 해를 끼치지 않고 파울 수 없습니다.

 

내보내기 설정 

 

내보내기 전에 Blender의 Smart UV Unwrap 옵션을 사용하려고 합니다. 너무 자세하게 설명하지 않으면 텍스처가 이상한 방식으로 스트레칭 하지 않고 모델의 다른 모양을 감싸면서 종횡비를 그대로 유지합니다 (자신의 모델을 만드는 경우에만 이 옵션을 읽는 것이 좋습니다) ).

 

모든 객체를 선택하고 변환을 적용하려고 합니다. 예를 들어, 스케일을 변경하거나 어떤 식으로든 변형 한 경우, 조금 축소 한 경우 여전히 크기가 32.445 %가 아니라 새로운 100 % 스케일이라고 블렌더에 알리는 것입니다.

 

File Format 

 

분명히 Three.js는 많은 3D 객체 파일 형식을 지원하지만 권장되는 것은 glTF (.glb)입니다. 블렌더는 이 형식을 내보내기 옵션으로 지원하므로 걱정할 필요가 없습니다.

 

 

2 부 : 환경 설정 

 

계속해서이 펜을 포크하거나 직접 펜을 시작하고 이 펜에서 CSS를 복사하십시오. 이 자습서에서 사용할 CSS 만 있는 빈 펜입니다.

 

https://codepen.io/kylewetton/pen/e77e5dd635f5d0299ae786047c7e6673

 

이것을 포크로 선택하지 않으면 HTML도 가져옵니다. 반응 형 메타 태그와 Google 글꼴이 포함되어 있습니다.

 

이 자습서에서는 세 가지 종속성을 사용합니다. 나는 그들이 하는 일을 설명하는 의견을 각각 위에 포함 시켰다. 맨 아래에 HTML로 복사하십시오.

<!-- The main Three.js file -->

<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/108/three.min.js'></script>

<!-- This brings in the ability to load custom 3D objects in the .gltf file format. Blender allows the ability to export to this format out the box -->

<script src='https://cdn.jsdelivr.net/gh/mrdoob/Three.js@r92/examples/js/loaders/GLTFLoader.js'></script>

<!-- This is a simple to use extension for Three.js that activates all the rotating, dragging and zooming controls we need for both mouse and touch, there isn't a clear CDN for this that I can find -->

<script src='https://threejs.org/examples/js/controls/OrbitControls.js'></script>

 

캔버스 요소를 포함 시켜 봅시다. 전체 3D 경험은 이 요소로 렌더링 되며 다른 모든 HTML은이 요소 주위에 UI가 됩니다. 캔버스를 HTML 하단의 종속성 위에 배치하십시오.

<!-- The canvas element is used to draw the 3D scene -->

<canvas id="c"></canvas>

 

이제 Three.js를 위한 새로운 Scene을 만들겠습니다. JavaScript에서 다음과 같이 이 장면을 참조하십시오.

// Init the scene

const scene = new THREE.Scene();

 

아래에서는 캔버스 요소를 참조하겠습니다.

const canvas = document.querySelector('#c');

 

Three.js를 실행하려면 몇 가지 사항이 필요하며 모든 기능을 사용할 수 있습니다. 첫 번째는 장면이고 두 번째는 렌더러입니다. 이것을 캔버스 참조 아래에 추가합시다. 새로운 WebGLRenderer를 만들고 캔버스를 캔버스에 전달하고 앤티 앨리어싱을 선택했습니다. 이렇게 하면 3D 모델 주위의 가장자리가 더 매끄럽게 됩니다.

// Init the renderer

const renderer = new THREE.WebGLRenderer({canvas, antialias: true});

 

이제 렌더러를 문서 본문에 추가하겠습니다.

document.body.appendChild(renderer.domElement);

 

캔버스 요소의 CSS는 본문의 높이와 너비를 100 %로 늘렸습니다. 이제 전체 캔버스가 검은 색이므로 전체 페이지가 검은 색으로 바뀝니다! 

 

우리의 장면은 검은색입니다. 우리는 올바른 길을 가고 있습니다.

 

Three.js가 필요로 하는 다음 것은 업데이트 루프입니다. 기본적으로 이것은 각 프레임 그리기에서 실행되는 함수이며 앱 작동 방식에 정말 중요합니다. 업데이트 함수 animate()를 호출했습니다. JavaScript에서 다른 모든 것 아래에 추가해 봅시다.

function animate() {
	renderer.render(scene, camera);
	requestAnimationFrame(animate);
}
animate();

 

 

여기서 카메라를 참조하고 있지만 아직 설정하지 않았습니다. 지금 하나 추가하겠습니다.

 

JavaScript 상단에 cameraFar라는 변수가 추가됩니다. 카메라를 장면에 추가하면 0,0,0 위치에 추가됩니다. 의자가 앉는 곳이 어디입니까! 따라서 cameraFar는 카메라가 이 마크에서 얼마나 멀리 움직여서 의자를 볼 수 있는지 알려주는 변수입니다. 

var cameraFar = 5;

 

이제 함수 위에 animate() {….}를 사용하여 카메라를 추가 할 수 있습니다.

// Add a camera

var camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = cameraFar;
camera.position.x = 0;

 

시야각이 50 인 전체 창 / 캔버스의 크기와 일부 기본 클리핑 평면이 있는 투시 카메라입니다. 평면은 오브젝트가 렌더링되지 않기 전에 카메라가 얼마나 가까이 또는 멀리 있어야 하는지 결정합니다. 앱에서 주의를 기울여야 할 것은 아닙니다.

 

 

장면은 여전히 ??검은 색입니다. 배경색을 설정하겠습니다.

 

상단의 장면 참조 위에 BACKGROUND_COLOR라는 배경색 변수를 추가하십시오.

const BACKGROUND_COLOR = 0xf1f1f1;

 

16 진수에서 # 대신 0x를 어떻게 사용했는지 주목하십시오. 

이것들은 16 진수이며, 기억해야 할 것은 JavaScript에서 표준 #hex 변수를 처리하는 방식이 문자열이 아니라는 것입니다. 정수이며 0x로 시작합니다. 

 

 

장면 참조 아래에서 장면 배경색을 업데이트하고 멀리 떨어진 곳에 같은 색의 안개를 추가해 보겠습니다. 바닥을 추가하면 바닥의 가장자리를 숨기는 데 도움이 됩니다.

const BACKGROUND_COLOR = 0xf1f1f1;

// Init the scene
const scene = new THREE.Scene();

// Set background
scene.background = new THREE.Color(BACKGROUND_COLOR );
scene.fog = new THREE.Fog(BACKGROUND_COLOR, 20, 100);

 

지금은 빈 세상입니다. 그러나 거기에는 아무것도 없고 그림자가 없기 때문에 말하기는 어렵습니다. 빈 장면이 있습니다. 이제 모델에 로드 할 차례입니다.

 

 

 

3 부 : 모델 로드 

 

모델에 로드 되는 함수를 추가하겠습니다.이 기능은 HTML에 추가 한 두 번째 종속성에 의해 제공됩니다.

 

그렇게 하기 전에 모델을 참조 해 보겠습니다.이 변수를 약간 사용하겠습니다. JavaScript 상단의 BACKGROUND_COLOR 위에 추가하십시오. 모델에 경로를 추가해 봅시다. 우리를 위해 호스팅 했으며 크기는 약 1MB입니다.

var theModel;

const MODEL_PATH =  "https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/chair.glb";

 

이제 새로운 로더를 생성하고 load 메소드를 사용할 수 있습니다. 모델을 3D 모델 전체 장면으로 설정합니다. 우리는 또한 이 앱의 크기를 설정할 것입니다. 올바른 크기는 로드 되는 것의 두 배 정도 인 것 같습니다. 셋째, y 위치를 -1만큼 오프셋 하여 조금 아래로 내리고 마지막으로 모델을 장면에 추가합니다.

 

첫 번째 매개 변수는 모델의 파일 ??경로이고, 두 번째 매개 변수는 일단 자원이 로드 된 후에 실행되는 함수이고, 세 번째 매개 변수는 현재 정의되지 않았지만 자원이 로드 되는 동안 실행되는 두 번째 함수에 사용될 수 있으며 최종 매개 변수는 오류를 처리합니다.

 

이것을 카메라 아래에 추가하십시오.

// Init the object loader

var loader = new THREE.GLTFLoader();
loader.load(MODEL_PATH, function(gltf) {
	theModel = gltf.scene;

	// Set the models initial scale   
	theModel.scale.set(2,2,2);

	// Offset the y position a bit
	theModel.position.y = -1;

	// Add the model to the scene
	scene.add(theModel);

}, undefined, function(error) {
	console.error(error)
});

 

 

이 시점에서 검은 색 픽셀로 늘어진 의자가 보입니다. 보기에는 끔찍하지만 지금까지는 옳습니다. 걱정하지 마세요!

 

 

 

카메라와 함께 조명이 필요합니다. 배경은 조명의 영향을 받지 않지만 지금 바닥을 추가하면 검은 색 (어두움)이 됩니다. Three.js에는 여러 가지 조명이 있으며 모든 조명을 조정할 수 있는 여러 가지 옵션이 있습니다. 반구 조명과 방향 조명의 두 가지를 추가하겠습니다. 설정은 앱에 따라 정렬되며 위치와 강도가 포함됩니다. 자신의 앱에서 이러한 방법을 채택한 경우 이 문제를 해결해야 하지만 지금은 포함 된 방법을 사용하겠습니다. 이 조명을 로더 아래에 추가하십시오.

// Add lights
var hemiLight = new THREE.HemisphereLight( 0xffffff, 0xffffff, 0.61 );
hemiLight.position.set( 0, 50, 0 );

// Add hemisphere light to scene   
scene.add( hemiLight );
var dirLight = new THREE.DirectionalLight( 0xffffff, 0.54 );
dirLight.position.set( -8, 12, 8 );
dirLight.castShadow = true;
dirLight.shadow.mapSize = new THREE.Vector2(1024, 1024);

// Add directional Light to scene    
scene.add( dirLight );

 

의자가 조금 더 좋아 보입니다! 계속하기 전에 지금까지 JavaScript가 있습니다.

var cameraFar = 5;

var theModel;

const MODEL_PATH =  "https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/chair.glb";

const BACKGROUND_COLOR = 0xf1f1f1;

// Init the scene

const scene = new THREE.Scene();

// Set background

scene.background = new THREE.Color(BACKGROUND_COLOR );

scene.fog = new THREE.Fog(BACKGROUND_COLOR, 20, 100);

const canvas = document.querySelector('#c');

// Init the renderer

const renderer = new THREE.WebGLRenderer({canvas, antialias: true});

document.body.appendChild(renderer.domElement);

// Add a camerra

var camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 1000);

camera.position.z = cameraFar;

camera.position.x = 0;

// Init the object loader

var loader = new THREE.GLTFLoader();

loader.load(MODEL_PATH, function(gltf) {

theModel = gltf.scene;

// Set the models initial scale   

theModel.scale.set(2,2,2);

// Offset the y position a bit

theModel.position.y = -1;

// Add the model to the scene

scene.add(theModel);

}, undefined, function(error) {

console.error(error)

});

// Add lights

var hemiLight = new THREE.HemisphereLight( 0xffffff, 0xffffff, 0.61 );

hemiLight.position.set( 0, 50, 0 );

// Add hemisphere light to scene   

scene.add( hemiLight );

var dirLight = new THREE.DirectionalLight( 0xffffff, 0.54 );

dirLight.position.set( -8, 12, 8 );

dirLight.castShadow = true;

dirLight.shadow.mapSize = new THREE.Vector2(1024, 1024);

// Add directional Light to scene    

scene.add( dirLight );

function animate() {

renderer.render(scene, camera);

requestAnimationFrame(animate);

}

animate();

 

다음은 현재 살펴볼 내용입니다.

 

 

 

픽셀 화와 스트레칭을 수정합시다. Three.js는 캔버스가 이동할 때 캔버스 크기를 업데이트 해야 하며 내부 해상도를 캔버스의 크기 뿐만 아니라 화면의 장치 픽셀 비율 (휴대 전화에서 훨씬 높음)으로 설정해야 합니다.

 

animate()를 호출하는 JavaScript의 맨 아래로 이동하여 이 함수를 추가하십시오. 이 함수는 기본적으로 캔버스 크기와 창 크기를 모두 듣고 두 크기가 같은지 여부에 따라 부울을 반환합니다. 애니메이션 함수 내부에서 해당 함수를 사용하여 장면을 다시 렌더링 할지 여부를 결정합니다. 이 기능은 또한 휴대 전화에서도 캔버스가 선명하도록 장치 픽셀 비율을 고려합니다.

 

JavaScript 하단에 이 함수를 추가하십시오.

function resizeRendererToDisplaySize(renderer) {

	const canvas = renderer.domElement;
	var width = window.innerWidth;
	var height = window.innerHeight;
	var canvasPixelWidth = canvas.width / window.devicePixelRatio;
	var canvasPixelHeight = canvas.height / window.devicePixelRatio;
	const needResize = canvasPixelWidth !== width || canvasPixelHeight !== height;

	if (needResize) {
		renderer.setSize(width, height, false);
	}

	return needResize;
}

 

이제 다음과 같이 애니메이션 함수를 업데이트하십시오.

function animate() {
	renderer.render(scene, camera);
	requestAnimationFrame(animate);
	if (resizeRendererToDisplaySize(renderer)) {
		const canvas = renderer.domElement;
		camera.aspect = canvas.clientWidth / canvas.clientHeight;
		camera.updateProjectionMatrix();
	}
}

 

즉시, 우리 의자가 훨씬 좋아 보입니다!

 

 

 

계속하기 전에 몇 가지 사항을 언급해야 합니다.

 

  • 의자가 거꾸로 있어요 우리는 단순히 Y 위치에서 모델을 회전 시킵니다
  • 지지대가 검은 색입니까? 그러나 나머지는 흰색입니까? 모델에 Blender에서 설정 한 재질 정보를 가져 오기 때문입니다. 우리는 앱에서 텍스처를 정의하고 모델이 로드 될 때 의자의 다른 영역에 추가 할 수 있는 기능을 추가 할 것이기 때문에 중요하지 않습니다. 따라서 나무 질감과 데님 질감 (스포일러 : 우리는)을 가지고 있다면 사용자가 선택할 필요 없이 하중에 설정할 수 있습니다. 이제 의자에 있는 재료는 그다지 중요하지 않습니다.

 

유머 감각, 로더 기능으로 가서 스케일을 (2,2,2)로 설정 한 위치를 기억하십니까? 아래에 이것을 추가하자 :

// Set the models initial scale

theModel.scale.set(2,2,2);

theModel.rotation.y = Math.PI;

 

예, 훨씬 낫습니다 한 가지 더 : Three.js는 내가 아는 한 학위를 지원하지 않지만 (?) 모두가 Math.PI를 사용하는 것으로 보입니다. 이것은 180도이므로 45도 각도로 무언가를 원하면 Math.PI / 4를 사용합니다.

 

 

 

좋아, 우리는 거기에 도착하고 있다! 하지만 바닥이 없으면 바닥이 없어도 그림자가 없어요?

 

바닥을 추가합니다. 여기서 하는 것은 새 평면 (2 차원 모양 또는 높이가 없는 3 차원 모양)을 만드는 것입니다.

 

조명 아래에 이것을 추가하십시오…

// Floor

var floorGeometry = new THREE.PlaneGeometry(5000, 5000, 1, 1);

var floorMaterial = new THREE.MeshPhongMaterial({

color: 0xff0000,

shininess: 0

});

var floor = new THREE.Mesh(floorGeometry, floorMaterial);

floor.rotation.x = -0.5 * Math.PI;

floor.receiveShadow = true;

floor.position.y = -1;

scene.add(floor);

 

여기서 무슨 일이 일어나는지 봅시다.

 

먼저 지오메트리를 만들었습니다.이 자습서에서는 Three.js에서 다른 지오메트리를 만들 필요는 없지만 모든 종류를 만들 수 있습니다.

 

둘째, 새로운 MeshPhongMaterial을 어떻게 만들고 몇 가지 옵션을 설정했는지 확인하십시오. 색깔이 있고 빛납니다. 나중에 Three.js 기타 자료를 확인하십시오. Phong은 반사성과 반사 하이라이트를 조정할 수 있기 때문에 좋습니다. 금속 및 주변 폐색과 같은 고급 텍스처 측면을 지원하는 MeshStandardMaterial도 있고 그림자를 지원하지 않는 MeshBasicMaterial도 있습니다. 이 튜토리얼에서 Phong 머티리얼을 만들겠습니다.

 

floor라는 변수를 만들고 지오메트리와 재질을 메시에 병합했습니다.

 

바닥의 ??회전을 평평하게 설정하고 그림자를 받는 기능을 선택하고 의자를 아래로 이동 한 것과 같은 방식으로 아래로 이동 한 다음 장면에 추가했습니다.

 

우리는 지금 이것을 보고 있어야 합니다 :

 

 

 

지금은 빨간색으로 남겨 두겠지만 그림자는 어디에 있습니까? 이를 위해 몇 가지 해야 할 일이 있습니다. 먼저 const 렌더러에서 몇 가지 옵션을 포함 시켜 보겠습니다.

// Init the renderer

const renderer = new THREE.WebGLRenderer({canvas, antialias: true});

renderer.shadowMap.enabled = true;

renderer.setPixelRatio(window.devicePixelRatio); 

 

픽셀 비율을 기기의 픽셀 비율에 관계없이 그림자와 관련이 없는 것으로 설정했습니다. 그러나 그곳에 있는 동안 그렇게 하겠습니다. shadowMap도 활성화했지만 여전히 그림자가 없습니까? 의자에 있는 재료가 Blender에서 가져온 재료이기 때문에 앱에서 일부를 작성하려고 하기 때문입니다.

 

로더 기능에는 3D 모델을 통과하는 기능이 포함됩니다. 로더 함수로 가서 theModel = gltf.scene 아래에 추가하십시오. 선. 3D 모델의 각 객체 (다리, 쿠션 등)에 대해 그림자를 투사하고 그림자를 받는 옵션을 사용할 수 있게 됩니다. 이 트래버스 방법은 나중에 다시 사용됩니다.

 

Model = gltf.scene; 아래에 이 줄을 추가하십시오.

theModel.traverse((o) => {

if (o.isMesh) {

o.castShadow = true;

o.receiveShadow = true;

}

});

 

그것은 이전보다 틀림없이 더 나빠 보이지만 최소한 바닥에 그림자가 있습니다! 모델에 여전히 Blender에서 가져온 재질이 있기 때문입니다. 이 모든 재료를 기본 흰색 PhongMaterial로 교체하겠습니다.

 

다른 PhongMaterial을 만들어 로더 함수 위에 추가 할 수 있습니다.

// Initial material

const INITIAL_MTL = new THREE.MeshPhongMaterial( { color: 0xf1f1f1, shininess: 10 } );

 

 

이것은 훌륭한 출발 물질이며 약간 미색이며 약간 반짝입니다. 시원한!

 

우리는 이것을 의자에 추가하고 완성 할 수 있지만, 어떤 물체는 적재 할 때 특정 색상이나 질감이 필요할 수 있으며, 모든 것을 동일한 기본 색상, 우리가 가는 방식으로 덮을 수는 없습니다. 이것은 우리의 초기 머티리얼 아래에 이 객체 배열을 추가하는 것입니다.

// Initial material

const INITIAL_MTL = new THREE.MeshPhongMaterial( { color: 0xf1f1f1, shininess: 10 } );

const INITIAL_MAP = [

{childID: "back", mtl: INITIAL_MTL},

{childID: "base", mtl: INITIAL_MTL},

{childID: "cushions", mtl: INITIAL_MTL},

{childID: "legs", mtl: INITIAL_MTL},

{childID: "supports", mtl: INITIAL_MTL},

];

3D 모델을 다시 탐색하고 childID를 사용하여 의자의 다른 부분을 찾아 재료를 적용합니다 (mtl 속성에 설정). 이 childID는 Blender에서 각 객체에 부여한 이름과 일치합니다. 해당 섹션을 읽으면 정보를 고려하십시오!

 

로더 함수 아래에 모델, 객체의 일부 (유형) 및 재질을 가져 와서 재질을 설정하는 기능을 추가하겠습니다. 또한 나중에 참조 할 수 있도록 nameID라는 이 부분에 새 속성을 추가 할 것입니다.

// Function - Add the textures to the models

function initColor(parent, type, mtl) {

  parent.traverse((o) => {

    if (o.isMesh) {

      if (o.name.includes(type)) {

        o.material = mtl;

        o.nameID = type; // Set a new property to identify this object

      }

    }

  });

}

 

이제 로더 함수 내에서 장면에 모델을 추가하기 직전에 (scene.add (theModel);)

 

INITIAL_MAP 배열의 각 객체에 대해 해당 기능을 실행 해 보겠습니다.

// Set initial textures

for (let object of INITIAL_MAP) {

	initColor(theModel, object.childID, object.mtl);

}

 

 

마지막으로 바닥으로 돌아가서 색상을 빨간색 (0xff0000)에서 밝은 회색 (0xeeeeee)으로 변경합니다.

// Floor

var floorGeometry = new THREE.PlaneGeometry(5000, 5000, 1, 1);

var floorMaterial = new THREE.MeshPhongMaterial({

  color: 0xeeeeee, // <------- Here

  shininess: 0

});

 

여기서 0xeeeeee은 배경색과 다르다는 것을 언급 할 가치가 있습니다. 조명이 켜진 바닥이 밝은 배경색과 일치 할 때까지 수동으로 전화를 걸었습니다. 우리는 지금 이것을 보고 있습니다 :

 

https://codepen.io/kylewetton/pen/41591db8dead5b15307c0cb4bad53923

 

축하합니다! 아무 데나 붙어 있으면 이 펜을 포크하거나 문제를 찾을 때까지 조사하십시오.

 

 

 

파트 4 : 컨트롤 추가 

 

실제로 이것은 매우 작은 부분이며 세 번째 종속성 OrbitControls.js 덕분에 매우 쉽습니다.

 

애니메이션 함수 위에 이것을 컨트롤에 추가합니다 :

// Add controls

var controls = new THREE.OrbitControls( camera, renderer.domElement );

controls.maxPolarAngle = Math.PI / 2;

controls.minPolarAngle = Math.PI / 3;

controls.enableDamping = true;

controls.enablePan = false;

controls.dampingFactor = 0.1;

controls.autoRotate = false; // Toggle this if you'd like the chair to automatically rotate

controls.autoRotateSpeed = 0.2; // 30

 

애니메이션 함수 내부의 상단에 다음을 추가하십시오.

controls.update();

 

컨트롤 변수는 새로운 OrbitControls 클래스입니다. 원하는 경우 여기에서 변경할 수 있는 몇 가지 옵션을 설정했습니다. 여기에는 사용자가 의자를 중심으로 (위와 아래) 회전 할 수 있는 범위가 포함됩니다. 의자를 중앙에 유지하기 위해 패닝을 비활성화하고, 무게를 줄 수 있도록 완충 기능을 사용했으며, 사용하도록 선택한 경우 자동 회전 기능을 포함 시켰습니다. 현재 false로 설정되어 있습니다.

 

의자를 클릭하고 드래그 하면 전체 마우스 및 터치 기능으로 모델을 탐색 할 수 있습니다!

 

https://codepen.io/kylewetton/pen/1aec88ab21048e7eab9a2864757887e5

 

 

 

5 부 : 색상 변경 

 

우리 앱은 현재 아무 것도 하지 않으므로 이 부분에서는 색상 변경에 중점을 둘 것입니다. HTML을 조금 더 추가하겠습니다. 나중에 CSS가하는 일에 대해 조금 설명하겠습니다.

 

캔버스 요소 아래에 이것을 추가하십시오.

<div class="controls">

<!-- This tray will be filled with colors via JS, and the ability to slide this panel will be added in with a lightweight slider script (no dependency used for this) -->

<div id="js-tray" class="tray">

<div id="js-tray-slide" class="tray__slide"></div>

</div>

</div>

기본적으로 .controls DIV는 화면 하단에 붙어 있고 .tray는 100 % 너비로 설정되어 있지만 자식 인 .tray__slide는 견본으로 채워지고 필요한 만큼 넓을 수 있습니다. 이 튜토리얼의 마지막 단계 중 하나로 이 아이를 슬라이드하여 색상을 탐색하는 기능을 추가 할 것입니다.

 

두 가지 색상을 추가하여 시작하겠습니다. JavaScript의 맨 위에 각각 color 속성을 가진 5 개의 객체 배열을 추가합니다.

const colors = [ { color: '66533C' }, { color: '173A2F' }, { color: '153944' }, { color: '27548D' }, { color: '438AAC' } ]

 

 

16 진수를 나타내는 # 또는 0x는 없습니다. 우리는 이 색상들을 두 기능 모두에 사용할 것입니다. 또한 이 색상에 광택 또는 질감 이미지 (스포일러 : 우리는, 우리는)와 같은 다른 속성을 추가 할 수 있기 때문에 객체입니다.

 

이 색상으로 견본을 만들 수 있습니다!

 

먼저 JavaScript 상단에 있는 트레이 슬라이더를 참조하십시오.

const TRAY = document.getElementById('js-tray-slide');

 

JavaScript의 맨 아래에 buildColors라는 새 함수를 추가하고 즉시 호출 할 수 있습니다.

// Function - Build Colors

function buildColors(colors) {

  for (let [i, color] of colors.entries()) {

    let swatch = document.createElement('div');

    swatch.classList.add('tray__swatch');

    swatch.style.background = "#" + color.color;

    swatch.setAttribute('data-key', i);

    TRAY.append(swatch);

  }

}

buildColors(colors);

 

 

 

이제 색상 배열에서 견본을 만들고 있습니다! data-key 속성을 견본으로 설정 한 다음,이를 사용하여 색상을 찾아 재료로 만듭니다.

 

새 buildColors 함수 아래에서 견본에 이벤트 핸들러를 추가해 보겠습니다.

// Swatches

const swatches = document.querySelectorAll(".tray__swatch");

for (const swatch of swatches) {

  swatch.addEventListener('click', selectSwatch);

}

 

클릭 핸들러는 selectSwatch라는 함수를 호출합니다. 이 함수는 컬러로 새로운 PhongMaterial을 빌드하고 다른 함수를 호출하여 3D 모델을 통과하고 변경하려는 부분을 찾아 업데이트합니다!

 

방금 추가 한 이벤트 핸들러 아래에 selectSwatch 함수를 추가하십시오.

function selectSwatch(e) {

  let color = colors[parseInt(e.target.dataset.key)];

  let new_mtl;

  new_mtl = new THREE.MeshPhongMaterial({

    color: parseInt('0x' + color.color),

    shininess: color.shininess ? color.shininess : 10

  });

  setMaterial(theModel, 'legs', new_mtl);

}

 

이 함수는 data-key 속성으로 색상을 찾고 새로운 재료를 만듭니다.

 

아직 작동하지 않으므로 setMaterial 함수를 추가해야 합니다 (방금 추가 한 함수의 마지막 줄 참조).

 

이 줄에 유의하십시오 : setMaterial (theModel,‘legs’, new_mtl) ;. 현재 이 함수에 '다리'를 전달하고 있으며 곧 업데이트하려는 다른 섹션을 변경하는 기능을 추가 할 예정입니다. 그러나 먼저 zcode> setMaterial을 추가하십시오. 

 

함수.

 

이 함수 아래에 setMaterial 함수를 추가하십시오.

function setMaterial(parent, type, mtl) {

  parent.traverse((o) => {

    if (o.isMesh && o.nameID != null) {

      if (o.nameID == type) {

        o.material = mtl;

      }

    }

  });

}

 

이 함수는 initColor 함수와 비슷하지만 약간의 차이가 있습니다. initColor에 추가 한 nameID를 확인하고 매개 변수 유형과 동일한 경우 재질을 추가합니다.

 

우리의 견본은 이제 새로운 재료를 만들고 다리의 색을 바꾸어 갈 수 있습니다! 여기 펜에 담긴 모든 것이 있습니다. 길을 잃은 경우 조사하십시오.

 

https://codepen.io/kylewetton/pen/db96f926342f5ef19399e022ff2088d4

 

 

6 부 : 변경할 부품 선택 

 

이제 다리의 색을 바꿀 수 있습니다. 그러나 견본이 재료에 추가해야 하는 부분을 선택할 수 있는 기능을 추가하겠습니다. 여는 HTML 태그 바로 아래에 이 HTML을 포함 시킵니다. 아래 CSS를  설명하겠습니다.

<!-- These toggle the the different parts of the chair that can be edited, note data-option is the key that links to the name of the part in the 3D file -->

<div class="options">

<div class="option --is-active" data-option="legs">

<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/legs.svg" alt=""/>

</div>

<div class="option" data-option="cushions">

<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/cushions.svg" alt=""/>

</div>

<div class="option" data-option="base">

<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/base.svg" alt=""/>

</div>

<div class="option" data-option="supports">

<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/supports.svg" alt=""/>

</div>

<div class="option" data-option="back">

<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/back.svg" alt=""/>

</div>

</div>

 

각각에 사용자 정의 아이콘이 있는 버튼 모음일 뿐입니다. .options DIV는 CSS를 통해 화면의 측면에 붙어 있으며 미디어 쿼리로 약간 이동합니다. 각 .option DIV는 ?is-active 클래스가 추가 될 때 빨간색 테두리가 있는 흰색 사각형입니다. 또한 nameID와 일치하는 데이터 옵션 속성이 포함되어 있으므로 이를 식별 할 수 있습니다. 마지막으로 이미지 요소에는 pointer-events : none이라는 CSS 속성이 있으므로 이미지를 클릭하더라도 이벤트가 부모에 유지됩니다.

 

 

 

JavaScript 상단에 activeOptions라는 다른 변수를 추가하고 기본적으로 'legs'로 설정하겠습니다.

var activeOption = 'legs';

 

 

이제 selectSwatch 함수로 돌아가 하드 코딩 된 'legs'매개 변수를 activeOption으로 업데이트하십시오.

setMaterial(theModel, activeOption, new_mtl);

 

 

이제 옵션을 클릭 할 때 activeOption을 변경하는 이벤트 핸들러를 작성하기 만하면 됩니다!

 

const 견본과 selectSwatch 함수 위에 이것을 추가하겠습니다.

// Select Option

const options = document.querySelectorAll(".option");

for (const option of options) {

option.addEventListener('click',selectOption);

}

function selectOption(e) {

let option = e.target;

activeOption = e.target.dataset.option;

for (const otherOption of options) {

otherOption.classList.remove('--is-active');

}

option.classList.add('--is-active');

}

 

selectOption 함수를 추가했습니다. activeOption을 이벤트 대상 데이터 옵션 값으로 설정하고 ?is-active 클래스를 토글합니다. 그게 다야!

 

https://codepen.io/kylewetton/pen/e28a2214f82dd05b29fd6a07b4b2b23a

 

근데 왜 여기서 멈춰? 물체는 어떤 것처럼 보일 수 있습니다. 모두 같은 재료 일 수는 없습니다. 나무 나 천이 없는 의자? 색상 선택을 조금 확장 해 봅시다. 색상 배열을 다음과 같이 업데이트하십시오.

const colors = [
{
	texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/wood.jpg',
	size: [2,2,2],
	shininess: 60
},
{
	texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/denim.jpg',
	size: [3, 3, 3], 
	shininess: 0
}, 
{ color: '66533C' }, { color: '173A2F' }, { color: '153944' }, { color: '27548D' }, { color: '438AAC' }
]

 

상단 2 개는 이제 텍스처입니다. 나무와 데님이 있습니다. 또한 크기와 광택의 두 가지 새로운 속성이 있습니다. 크기는 패턴을 반복하는 빈도이므로 숫자가 클수록 패턴이 더 조밀 해지거나 더 간단하게 반복됩니다.

 

이 기능을 추가하기 위해 업데이트 해야 하는 두 가지 함수가 있습니다. 먼저 buildColors 함수로 가서 이것을 업데이트하십시오.

// Function - Build Colors

function buildColors(colors) {

for (let [i, color] of colors.entries()) {

let swatch = document.createElement('div');

swatch.classList.add('tray__swatch');

if (color.texture)

{

swatch.style.backgroundImage = "url(" + color.texture + ")";

} else

{

swatch.style.background = "#" + color.color;

}

swatch.setAttribute('data-key', i);

TRAY.append(swatch);

}

}

 

이제 텍스처인지 확인하고 있다면, 견본 배경을 그 텍스처로 깔끔하게 설정합니다!

 

 

 

두 번째로 업데이트 할 함수는 selectSwatch 함수입니다. 이것을 다음과 같이 업데이트하십시오.

function selectSwatch(e) {

let color = colors[parseInt(e.target.dataset.key)];

let new_mtl;

if (color.texture) {

let txt = new THREE.TextureLoader().load(color.texture);

txt.repeat.set( color.size[0], color.size[1], color.size[2]);

txt.wrapS = THREE.RepeatWrapping;

txt.wrapT = THREE.RepeatWrapping;

new_mtl = new THREE.MeshPhongMaterial( {

map: txt,

shininess: color.shininess ? color.shininess : 10

});

}

else

{

new_mtl = new THREE.MeshPhongMaterial({

color: parseInt('0x' + color.color),

shininess: color.shininess ? color.shininess : 10

});

}

setMaterial(theModel, activeOption, new_mtl);

}

 

여기서 무슨 일이 일어나고 있는지 설명하기 위해 이 함수는 텍스처인지 확인합니다. 그렇다면 Three.js TextureLoader 메서드를 사용하여 새 텍스처를 만들고 크기 값을 사용하여 텍스처 반복을 설정하고 래핑을 설정합니다 (이 래핑 옵션이 가장 잘 작동하는 것 같습니다. 그런 다음 PhongMaterials 맵 속성을 텍스처로 설정하고 마지막으로 광택 값을 사용합니다. 

 

텍스처가 아닌 경우 이전 방법을 사용합니다. 광택 특성을 원래 색상 중 하나로 설정할 수 있습니다!

 

 

 

중요 : 텍스처를 추가하려고 할 때 텍스처가 검은 색으로 남아있는 경우. 콘솔을 확인하십시오. 도메인 간 CORS 오류가 발생합니까? 이것은 CodePen 버그이며 문제를 해결하기 위해 최선을 다했습니다. 이러한 자산은 Pro 기능을 통해 CodePen에서 직접 호스팅되므로 불행히도이 문제와 싸워야 합니다. 분명히 여기에 가장 좋은 방법은 해당 이미지 URL을 직접 방문하지 않는 것입니다. 그렇지 않으면 Cloudinary에 가입하고 프리 티어를 사용하는 것이 좋습니다. 여기서 텍스처를 가리킬 수 있습니다. 

 

최소한 내 질감이 작동하는 펜은 다음과 같습니다.

 

https://codepen.io/kylewetton/pen/b718cc21f4dd60b019290fe33ad0da10

 

 

 

7 부 : 마무리 작업 

 

나는 큰 버튼을 눌러 고객에게 프로젝트를 넘겨 받았으며, 유혹에 찬 유혹을 가졌으며 동료들 (Dave from Accounts)은 어떻게 그들에 대한 피드백으로 돌아 왔습니다. 눌러야 할 것이 무엇인지 몰랐습니다 (데이브, 나사).

 

클릭 유도 문안을 추가해 보겠습니다. 먼저 canvas 요소 위에 HTML 패치를 삽입 해 보겠습니다.

<!-- Just a quick notice to the user that it can be interacted with -->

<span class="drag-notice" id="js-drag-notice">Drag to rotate 360&#176;</span>

 

CSS는 이 클릭 유도 문안을 의자 위에 놓아 사용자가 의자를 회전하도록 드래그 할 수 있는 큰 버튼입니다. 그래도 거기 머물러 있습니까? 우리는 그것을 얻을 것입니다.

 

의자가 먼저 장착되면 회전 한 다음 회전이 완료되면 해당 클릭 유도 문안을 숨기겠습니다.

 

먼저 JavaScript 상단에 로드 된 변수를 추가하고 false로 설정하십시오.

var loaded = false;

 

자바 스크립트 맨 아래에 이 함수를 추가하십시오.

// Function - Opening rotate

let initRotate = 0;

function initialRotation() {

initRotate++;

if (initRotate <= 120) {

theModel.rotation.y += Math.PI / 60;

} else {

loaded = true;

}

}

 

이것은 단순히 120 프레임 (60fps에서 약 2 초) 내에서 모델을 360도 회전 시키며, 애니메이션 기능에서 이를 실행하여 120 프레임 동안 호출합니다. 우리의 애니메이션 기능에서 사실로. 다음은 마지막에 새 코드를 사용하여 전체적으로 표시되는 방법입니다.

function animate() {

controls.update();

renderer.render(scene, camera);

requestAnimationFrame(animate);

if (resizeRendererToDisplaySize(renderer)) {

const canvas = renderer.domElement;

camera.aspect = canvas.clientWidth / canvas.clientHeight;

camera.updateProjectionMatrix();

}

if (theModel != null && loaded == false) {

initialRotation();

}

}

animate();

 

Model이 null과 같지 않고 로드 된 변수가 false인지 확인하고 120 프레임 동안 해당 함수를 실행합니다.이 시점에서 함수는 loaded = true로 전환되고 애니메이션 함수는 이를 무시합니다.

 

당신은 좋은 회전 의자가 있어야 합니다. 의자가 멈출 때 행동 유도를 제거하기에 좋은 시간입니다.

 

CSS에는 애니메이션으로 숨길 수 있는 클릭 유도 문안에 추가 할 수 있는 클래스가 있습니다.이 애니메이션은 3 초의 지연이 있으므로 회전이 시작되는 동시에 해당 클래스를 추가해 보겠습니다.

 

JavaScript 상단에서 다음을 참조합니다.

const DRAG_NOTICE = document.getElementById('js-drag-notice');

 

애니메이션 함수를 다음과 같이 업데이트하십시오.

if (theModel != null && loaded == false) {

initialRotation();

DRAG_NOTICE.classList.add('start');

}

 

여기 더 많은 색상이 있습니다. 색상 배열을 업데이트하십시오. 그 아래에 가벼운 슬라이딩 함수가 있습니다.

const colors = [

{

texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/wood_.jpg',

size: [2,2,2],

shininess: 60

},

{

texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/fabric_.jpg',

size: [4, 4, 4],

shininess: 0

},

{

texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/pattern_.jpg',

size: [8, 8, 8],

shininess: 10

},

{

texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/denim_.jpg',

size: [3, 3, 3],

shininess: 0

},

{

texture: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/1376484/quilt_.jpg',

size: [6, 6, 6],

shininess: 0

},

{ color: '131417' }, { color: '374047' }, { color: '5f6e78' }, { color: '7f8a93' }, { color: '97a1a7' }, { color: 'acb4b9' }, { color: 'DF9998', }, { color: '7C6862' }, { color: 'A3AB84' }, { color: 'D6CCB1' }, { color: 'F8D5C4' }, { color: 'A3AE99' }, { color: 'EFF2F2' }, { color: 'B0C5C1' }, { color: '8B8C8C' }, { color: '565F59' }, { color: 'CB304A' }, { color: 'FED7C8' }, { color: 'C7BDBD' }, { color: '3DCBBE' }, { color: '264B4F' }, { color: '389389' }, { color: '85BEAE' }, { color: 'F2DABA' }, { color: 'F2A97F' }, { color: 'D85F52' }, { color: 'D92E37' }, { color: 'FC9736' }, { color: 'F7BD69' }, { color: 'A4D09C' }, { color: '4C8A67' }, { color: '25608A' }, { color: '75C8C6' }, { color: 'F5E4B7' }, { color: 'E69041' }, { color: 'E56013' }, { color: '11101D' }, { color: '630609' }, { color: 'C9240E' }, { color: 'EC4B17' }, { color: '281A1C' }, { color: '4F556F' }, { color: '64739B' }, { color: 'CDBAC7' }, { color: '946F43' }, { color: '66533C' }, { color: '173A2F' }, { color: '153944' }, { color: '27548D' }, { color: '438AAC' }

]

 

 

대박! JavaScript의 바로 아래에 페이지가 멈추고 이 기능을 추가하면 마우스와 터치로 견본 패널을 드래그 할 수 있습니다. 주제를 계속 유지하기 위해 나는 그것이 어떻게 작동 하는지에 대해서는 너무 많이 탐구하지 않을 것이다.

var slider = document.getElementById('js-tray'), sliderItems = document.getElementById('js-tray-slide'), difference;

function slide(wrapper, items) {

var posX1 = 0,

posX2 = 0,

posInitial,

threshold = 20,

posFinal,

slides = items.getElementsByClassName('tray__swatch');

// Mouse events

items.onmousedown = dragStart;

// Touch events

items.addEventListener('touchstart', dragStart);

items.addEventListener('touchend', dragEnd);

items.addEventListener('touchmove', dragAction);

function dragStart (e) {

e = e || window.event;

posInitial = items.offsetLeft;

difference = sliderItems.offsetWidth - slider.offsetWidth;

difference = difference * -1;

if (e.type == 'touchstart') {

posX1 = e.touches[0].clientX;

} else {

posX1 = e.clientX;

document.onmouseup = dragEnd;

document.onmousemove = dragAction;

}

}

function dragAction (e) {

e = e || window.event;

if (e.type == 'touchmove') {

posX2 = posX1 - e.touches[0].clientX;

posX1 = e.touches[0].clientX;

} else {

posX2 = posX1 - e.clientX;

posX1 = e.clientX;

}

if (items.offsetLeft - posX2 <= 0 && items.offsetLeft - posX2 >= difference) {

items.style.left = (items.offsetLeft - posX2) + "px";

}

}

function dragEnd (e) {

posFinal = items.offsetLeft;

if (posFinal - posInitial < -threshold) { } else if (posFinal - posInitial > threshold) {

} else {

items.style.left = (posInitial) + "px";

}

document.onmouseup = null;

document.onmousemove = null;

}

}

slide(slider, sliderItems);

 

이제 CSS로 이동하고 .tray__slider 아래에서 이 작은 애니메이션의 주석 처리를 제거하십시오.

/*   transform: translateX(-50%);

animation: wheelin 1s 2s ease-in-out forwards; */

 

 

자, 마지막 두 번의 터치로 마무리 해 봅시다.

이 추가 클릭 유도 문안을 포함하도록 .controls div를 업데이트하겠습니다.

<div class="controls">

<div class="info">

<div class="info__message">

<p><strong>&nbsp;Grab&nbsp;</strong> to rotate chair. <strong>&nbsp;Scroll&nbsp;</strong> to zoom. <strong>&nbsp;Drag&nbsp;</strong> swatches to view more.</p>

</div>

</div>

<!-- This tray will be filled with colors via JS, and the ability to slide this panel will be added in with a lightweight slider script (no dependency used for this) -->

<div id="js-tray" class="tray">

<div id="js-tray-slide" class="tray__slide"></div>

</div>

</div>

 

앱을 제어하는 ??방법에 대한 지침이 포함 된 새로운 정보 섹션이 있습니다.

 

마지막으로 모든 것이 로드 되는 동안 앱이 깨끗하도록 로드 오버레이를 추가하고 모델이 로드 되면 제거합니다.

 

body 태그 아래 HTML 상단에 추가하십시오.

<!-- The loading element overlays all else until the model is loaded, at which point we remove this element from the DOM -->  
<div class="loading" id="js-loader">
	<div class="loader"></div>
</div>

 

 

다음은 로더에 대한 것입니다. 먼저로드하기 위해 CSS에 포함하지 않고 헤드 태그에 CSS를 추가하겠습니다. 

따라서 닫는 head 태그 바로 위에 이 CSS를 추가하십시오.

<style>

.loading {

position: fixed;

z-index: 50;

width: 100%;

height: 100%;

top: 0; left: 0;

background: #f1f1f1;

display: flex;

justify-content: center;

align-items: center;

}

.loader{

-webkit-perspective: 120px;

-moz-perspective: 120px;

-ms-perspective: 120px;

perspective: 120px;

width: 100px;

height: 100px;

}

.loader:before{

content: "";

position: absolute;

left: 25px;

top: 25px;

width: 50px;

height: 50px;

background-color: #ff0000;

animation: flip 1s infinite;

}

@keyframes flip {

0% {

transform: rotate(0);

}

50% {

transform: rotateY(180deg);

}

100% {

transform: rotateY(180deg)  rotateX(180deg);

}

}

</style>

 

거의 다 왔어! 모델이 로드 되면 제거하겠습니다.

 

JavaScript 상단에서 참조하십시오 :

const LOADER = document.getElementById('js-loader');

 

그런 다음 loader 함수에서 scene.add (theModel) 뒤에 이 줄을 포함하십시오.

// Remove the loader
LOADER.remove();

 

 

이제 우리의 앱이 이 DIV 뒤에 로드 되어 다듬어집니다 :

 

 

 

그리고 그게 다야! 다음은 참조 용으로 완성 된 펜입니다.

 

 

 

원문 : https://tympanus.net/codrops/2019/09/17/how-to-build-a-color-customizer-app-for-a-3d-model-with-three-js/

 

 

+ Recent posts