Использование буфера трафарета (stencil buffer) в three.js

176
26 марта 2022, 12:30

Пытаюсь создать сцену с применением буфера трафарета, но к сожалению, по его использованию слишком мало информации и примеров. Планирую по центру каждой из сторон тэтраэдра вырезать круг или квадрат, это не важно. Подскажите, как это возможно реализовать, если можно поподробнее?

Ожидаемый результат на картинке, где окружности это вырезанные отверстия.

С добавлением webGL stencil

    window.onload = function() {
    // Создаем переменную канвас
    var canvas = document.getElementById("canvas");
    var scene, camera, renderer;

    // -------------- Создание объекта - сцена ----------------
    // Создание элемента сцена
    scene = new THREE.Scene();

    // -------------- Создание объекта - источник света ----------------
    var light = new THREE.AmbientLight(0xffffff, 0.5);
    scene.add(light);
    var light1 = new THREE.PointLight(0xffffff, 0.5);
    scene.add(light1);

    // -------------- Создание объекта - камера ----------------
    // Создание элемента камера с углом обхора 65 градусов и пропорций и близкость и дальность отображения объекта
    camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 3000);

    // -------------- Создание объекта - рендерер ----------------
    // Канва: канва - это настройка того, что функция будет передана в имеющийся элемент канвас, и не будет создаваться новый
    // antialias: true - это опция сглаживания углов у объектов
    renderer = new THREE.WebGLRenderer({canvas: canvas, antialias: true, alpha: true, stencil: true});
    // установка серого цвета на канвасе
    renderer.setClearColor(0x444444);
    // Устанавливаем соотношение пикселей устройства. Это обычно используется для устройства HiDPI, чтобы предотвратить размывание выходного холста.
    renderer.setPixelRatio(window.devicePixelRatio);
    // Установка сцены на всё окно 
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    // -------------- Создание объекта - тэтраэдр ----------------
    var tetrahedron = new THREE.TetrahedronGeometry( 100, 0 );
    var tetrahedronMaterial = new THREE.MeshStandardMaterial( {color: 0xF3FFE2, roughness: 0.5, metalness: 0.5} );
    var tetrahedronMesh = new THREE.Mesh( tetrahedron, tetrahedronMaterial );
    tetrahedronMesh.position.set(-150, 0, -1000);
    scene.add( tetrahedronMesh );
    // -------------- Создание объекта - сфера (трафарет буфер) ----------------
    var sphere = new THREE.SphereGeometry( 50, 32, 32 );
    var sphereMaterial = new THREE.MeshStandardMaterial( {color: 0x000000, roughness: 0.5, metalness: 0.5} );
    var sphereMesh = new THREE.Mesh( sphere, sphereMaterial );
    sphereMesh.position.set(-150, 0, -1000);
    scene.add( sphereMesh );

    // -------------- Создание объекта - цилиндр ----------------
    var cylinder = new THREE.CylinderGeometry( 25, 25, 75, 32 );
    var cylinderMaterial = new THREE.MeshStandardMaterial( {color: 0x228B22, roughness: 0.5, metalness: 0.5, opacity: 0.5, transparent: true} );
    var cylinderMesh = new THREE.Mesh( cylinder, cylinderMaterial );
    cylinderMesh.position.set( 100, 0, -1000);
    scene.add( cylinderMesh );

    // -------------- Создание объекта - тор ----------------
    var torus = new THREE.TorusGeometry( 31, 7, 16, 100 );
    var torusMaterial = new THREE.MeshStandardMaterial( {color: 0xADFF2F, roughness: 0.5, metalness: 0.5} );
    var torusMesh = new THREE.Mesh( torus, torusMaterial );
    torusMesh.position.set( 100, 0, -1000);
    torusMesh.rotation.x = 1.6;
    scene.add( torusMesh );
    // Установка позиции камера x, y, z
    camera.position.z = 300;

    // Создание функции анимация
    function animate() {
        requestAnimationFrame( animate );
        tetrahedronMesh.rotation.x += 0.01;
        tetrahedronMesh.rotation.y += 0.01;
        // cylinderMesh.rotation.z += 0.01;
        // cylinderMesh.rotation.y += 0.01;
        cylinderMesh.rotation.x += 0.01;
        torusMesh.rotation.x += 0.01;
        // torusMesh.rotation.y += 0.01;
        renderer.render( scene, camera );
    }
    animate();
}

Немного модернизировал свой код, добавив объект - сфера, который планируется использовать как трафарет.

window.onload = function() {
    // Создаем переменную канвас
    var canvas = document.getElementById("canvas");
    var scene, camera, renderer, backStencilScene;
    var gl = renderer;

    // -------------- Создание объекта - сцена ----------------
    // Создание элемента сцена
    scene = new THREE.Scene();
    backStencilScene  = new THREE.Scene();

    // -------------- Создание объекта - источник света ----------------
    var light = new THREE.AmbientLight(0xffffff, 0.5);
    scene.add(light);
    var light1 = new THREE.PointLight(0xffffff, 0.5);
    scene.add(light1);

    // -------------- Создание объекта - камера ----------------
    // Создание элемента камера с углом обхора 65 градусов и пропорций и близкость и дальность отображения объекта
    camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 3000);

    // -------------- Создание объекта - рендерер ----------------
    // Канва: канва - это настройка того, что функция будет передана в имеющийся элемент канвас, и не будет создаваться новый
    // antialias: true - это опция сглаживания углов у объектов
    renderer = new THREE.WebGLRenderer({canvas: canvas, antialias: true, alpha: true, stencil: true});
    // установка серого цвета на канвасе
    renderer.setClearColor(0x444444);
    // Устанавливаем соотношение пикселей устройства. Это обычно используется для устройства HiDPI, чтобы предотвратить размывание выходного холста.
    renderer.setPixelRatio(window.devicePixelRatio);
    // Установка сцены на всё окно 
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    // -------------- Создание объекта - тэтраэдр ----------------
    var tetrahedron = new THREE.TetrahedronGeometry( 100, 0 );
    var tetrahedronMaterial = new THREE.MeshStandardMaterial( {color: 0xF3FFE2, roughness: 0.5, metalness: 0.5} );
    var tetrahedronMesh = new THREE.Mesh( tetrahedron, tetrahedronMaterial );
    tetrahedronMesh.position.set(-150, 0, -1000);
    scene.add( tetrahedronMesh );
    // -------------- Создание объекта - сфера (трафарет буфер) ----------------
    var sphere = new THREE.SphereGeometry( 50, 32, 32 );
    var sphereMaterial = new THREE.MeshStandardMaterial( {color: 0x000000, roughness: 0.5, metalness: 0.5} );
    var sphereMesh = new THREE.Mesh( sphere, sphereMaterial );
    sphereMesh.position.set(-150, 0, -1000);
    // -------------- Создание объекта - цилиндр ----------------
    var cylinder = new THREE.CylinderGeometry( 25, 25, 75, 32 );
    var cylinderMaterial = new THREE.MeshStandardMaterial( {color: 0x228B22, roughness: 0.5, metalness: 0.5, opacity: 0.5, transparent: true} );
    var cylinderMesh = new THREE.Mesh( cylinder, cylinderMaterial );
    cylinderMesh.position.set( 100, 0, -1000);
    backStencilScene.add( cylinderMesh );

    // -------------- Создание объекта - тор ----------------
    var torus = new THREE.TorusGeometry( 31, 7, 16, 100 );
    var torusMaterial = new THREE.MeshStandardMaterial( {color: 0xADFF2F, roughness: 0.5, metalness: 0.5} );
    var torusMesh = new THREE.Mesh( torus, torusMaterial );
    torusMesh.position.set( 100, 0, -1000);
    torusMesh.rotation.x = 1.6;
    scene.add( torusMesh );
    // Установка позиции камера x, y, z
    camera.position.z = 300;

    // Создание функции анимация
    function animate() {
        renderer.clear();
        requestAnimationFrame( animate );
        // Включение буфера трафарета
        gl.enable(gl.STENCIL_TEST);
        // Настройка теста
        gl.stencilFunc(
            gl.ALWAYS,    // the test
            1,            // reference value
            0xFF          // mask
        );
        // Установление трафарета на эталонное значение, когда пройдут тесты трафарета и глубины
        gl.stencilOp(
            gl.KEEP,     // что делать если тест трафарета не пройден
            gl.KEEP,     // что делать если тест глубины не пройден
            gl.REPLACE   // что делать если оба теста не пройдены
        );
        scene.add( sphereMesh );
        // Тест пройдёт только если он равен нулю
        gl.stencilFunc(
            gl.EQUAL,     // Тест
            0,            // Исходная величина
            0xFF         // Маска
        );
        gl.stencilOp(
            gl.KEEP,     // что делать если тест трафарета не пройден
            gl.KEEP,     // что делать если тест глубины не пройден
            gl.KEEP      // что делать если оба теста не пройдены
        );


        tetrahedronMesh.rotation.x += 0.01;
        tetrahedronMesh.rotation.y += 0.01;
        // cylinderMesh.rotation.z += 0.01;
        // cylinderMesh.rotation.y += 0.01;
        cylinderMesh.rotation.x += 0.01;
        torusMesh.rotation.x += 0.01;
        // torusMesh.rotation.y += 0.01;
        renderer.render( scene, camera );
    }
    animate();
}
Answer 1

Надеюсь, что правильно понял поставленный вопрос :)

Отверстия разных форм в гранях тетраэдра можно сделать с помощью формообразующей функции по UV координатам.

Функция взята отсюда. Была так же применена в этом примере.

Трюк здесь в том, чтобы использовать свойство материала .alphaTest. В шейдере задаем для нужных нам фрагментов прозрачность 0, а дальше код шейдера материала сам уберет те фрагменты, для которых значение прозрачности .a меньше 0.5.

На всякий случай тут вот ссылка на jsfiddle.

Собственно, пример решения:

var scene = new THREE.Scene(); 
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); 
camera.position.set(0.5, 1, 2); 
var renderer = new THREE.WebGLRenderer(); 
renderer.setSize(window.innerWidth, window.innerHeight); 
//renderer.setClearColor(0x404040); 
document.body.appendChild(renderer.domElement); 
 
var controls = new THREE.OrbitControls(camera, renderer.domElement); 
 
scene.add(new THREE.GridHelper(2, 4, 0x404040, 0x404040)); 
 
// tetrahedron 
 
var pts = [ 
  new THREE.Vector3(Math.sqrt(8 / 9), 0, -(1 / 3)), 
  new THREE.Vector3(-Math.sqrt(2 / 9), Math.sqrt(2 / 3), -(1 / 3)), 
  new THREE.Vector3(-Math.sqrt(2 / 9), -Math.sqrt(2 / 3), -(1 / 3)), 
  new THREE.Vector3(0, 0, 1) 
]; 
 
var faces = [ 
  pts[0].clone(), pts[2].clone(), pts[1].clone(), 
  pts[0].clone(), pts[1].clone(), pts[3].clone(), 
  pts[1].clone(), pts[2].clone(), pts[3].clone(), 
  pts[2].clone(), pts[0].clone(), pts[3].clone() 
]; 
 
var geom = new THREE.BufferGeometry().setFromPoints(faces); 
geom.rotateX(-Math.PI * 0.5); 
 
geom.setAttribute("uv", new THREE.Float32BufferAttribute([ 
  0.5, 1, 0.06698729810778059, 0.2500000000000001, 0.9330127018922194, 0.2500000000000001, 
  0.06698729810778059, 0.2500000000000001, 0.9330127018922194, 0.2500000000000001, 0.5, 1, 
  0.06698729810778059, 0.2500000000000001, 0.9330127018922194, 0.2500000000000001, 0.5, 1, 
  0.06698729810778059, 0.2500000000000001, 0.9330127018922194, 0.2500000000000001, 0.5, 1 
], 2)); 
 
var matPts = new THREE.PointsMaterial({ 
  size: 0.1, 
  color: "red" 
}); 
var points = new THREE.Points(geom, matPts); 
scene.add(points); 
 
var mat = new THREE.MeshBasicMaterial({ 
  map: new THREE.TextureLoader().load("https://threejs.org/examples/textures/uv_grid_opengl.jpg"), 
  side: THREE.DoubleSide, 
  alphaTest: 0.5 
}); 
mat.defines = { 
  USE_UV: "" 
}; 
var uniforms = { 
  vSides: { 
    value: 5 
  } 
}; 
mat.onBeforeCompile = shader => { 
  console.log(shader.fragmentShader); 
  shader.uniforms.vSides = uniforms.vSides; 
    shader.fragmentShader = ` 
  	uniform float vSides; 
    ` + shader.fragmentShader; 
  shader.fragmentShader = shader.fragmentShader.replace( 
    `#include <clipping_planes_pars_fragment>`, 
    `#include <clipping_planes_pars_fragment> 
 
float getShape(float outer, vec2 uv){ 
    uv *= 2.0; 
    float a = atan(uv.x,-uv.y); 
    float r = 3.1415926 * 2. / vSides; 
    float d = cos( floor( .5 + a / r ) * r - a ) * length( uv ); 
    return step(outer, d); 
} 
` 
  ); 
  shader.fragmentShader = shader.fragmentShader.replace( 
    `vec4 diffuseColor = vec4( diffuse, opacity );`, 
    `vec4 diffuseColor = vec4( diffuse, opacity ); 
    diffuseColor.a = getShape(0.25, vUv - 0.5); 
    ` 
  ); 
} 
var mesh = new THREE.Mesh(geom, mat); 
scene.add(mesh); 
 
 
renderer.setAnimationLoop(() => { 
  renderer.render(scene, camera) 
});
body { 
  overflow: hidden; 
  margin: 0; 
}
<script src="https://threejs.org/build/three.min.js"></script> 
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>

PS Построил свой тетраэдр с гранями и UV координатами, так как мне нужно было точно знать, что UV координаты заданы так, как мне надо. Координаты точек взяты из Wiki. UV координаты нашел сам, главное, чтобы UV c величинами [0.5, 0.5] находилась в центре грани. Количество сторон для отверстий задается в этой униформе:

var uniforms = {
  vSides: {
    value: 5
  }
};
READ ALSO
Обновление стейта в цикле React

Обновление стейта в цикле React

Есть такой компонент:

83
Процесс создания классов

Процесс создания классов

Кто-нибудь может пожалуйста объяснить более доходчивым образом, почему так происходит? Как я понимаю, в момент создания класса,не создания...

124
Как применить действие кнопки по очереди к каждой строке с одинаковыми классами?

Как применить действие кнопки по очереди к каждой строке с одинаковыми классами?

при нажатии кнопки нужно чт бы копировался майл под которым она находитсяСейчас все копи рую первое значение

190
Проблема с List в Unity3D [закрыт]

Проблема с List в Unity3D [закрыт]

Хотите улучшить этот вопрос? Обновите вопрос так, чтобы он вписывался в тематику Stack Overflow на русском

121