diff --git a/editor/css/main.css b/editor/css/main.css index cb22e740c25fa4..d1beb3ad734e90 100644 --- a/editor/css/main.css +++ b/editor/css/main.css @@ -936,6 +936,78 @@ hr { margin-top: 15px; } +.TextureParametersDialog-content { + min-width: 480px; +} + +.TextureParametersDialog-split { + display: flex; + gap: 20px; +} + +.TextureParametersDialog-preview { + flex: 0 0 auto; +} + +.TextureParametersDialog-preview canvas { + border: 1px solid #888; + background: #000; + display: block; + max-width: 100%; + height: auto; +} + +.TextureParametersDialog-form { + flex: 1 1 auto; + min-width: 0; +} + +@media all and ( max-width: 600px ) { + + .TextureParametersDialog-content { + min-width: 0; + width: 90vw; + } + + .TextureParametersDialog-split { + flex-direction: column; + } + + .TextureParametersDialog-preview { + align-self: center; + max-width: 100%; + } + +} + +.TextureParametersDialog-groupHeading { + margin: 12px 0 6px; + padding-bottom: 2px; + border-bottom: 1px solid #ccc; + font-weight: bold; + text-transform: uppercase; + font-size: 11px; + color: #666; +} + +.TextureParametersDialog-groupHeading:first-child { + margin-top: 0; +} + +@media ( prefers-color-scheme: dark ) { + + .TextureParametersDialog-groupHeading { + border-bottom-color: #444; + color: #888; + } + +} + +.TextureSettingsButton { + padding: 0 4px; + line-height: 16px; +} + @media ( prefers-color-scheme: dark ) { .Dialog-content { diff --git a/editor/js/Sidebar.Material.MapProperty.js b/editor/js/Sidebar.Material.MapProperty.js index 864e23b37c5e26..484ab1a1132c93 100644 --- a/editor/js/Sidebar.Material.MapProperty.js +++ b/editor/js/Sidebar.Material.MapProperty.js @@ -1,11 +1,13 @@ import * as THREE from 'three'; -import { UICheckbox, UIDiv, UINumber, UIRow, UIText } from './libs/ui.js'; +import { UIButton, UICheckbox, UIDiv, UINumber, UIRow, UIText } from './libs/ui.js'; import { UITexture } from './libs/ui.three.js'; import { SetMaterialMapCommand } from './commands/SetMaterialMapCommand.js'; import { SetMaterialValueCommand } from './commands/SetMaterialValueCommand.js'; import { SetMaterialRangeCommand } from './commands/SetMaterialRangeCommand.js'; import { SetMaterialVectorCommand } from './commands/SetMaterialVectorCommand.js'; +import { SetTextureParametersCommand } from './commands/SetTextureParametersCommand.js'; +import { TextureParametersDialog } from './TextureParametersDialog.js'; function SidebarMaterialMapProperty( editor, property, name ) { @@ -20,6 +22,10 @@ function SidebarMaterialMapProperty( editor, property, name ) { const map = new UITexture( editor ).onChange( onMapChange ); container.add( map ); + const settings = new UIButton( '⚙' ).setClass( 'TextureSettingsButton' ).setMarginRight( '4px' ).onClick( onSettingsClick ); + settings.setDisabled( true ); + container.add( settings ); + const mapType = property.replace( 'Map', '' ); const colorMaps = [ 'map', 'emissiveMap', 'sheenColorMap', 'specularColorMap', 'envMap' ]; @@ -110,6 +116,26 @@ function SidebarMaterialMapProperty( editor, property, name ) { } + async function onSettingsClick() { + + const texture = map.getValue(); + if ( texture === null ) return; + + const dialog = new TextureParametersDialog( editor, texture ); + + try { + + const parameters = await dialog.show(); + editor.execute( new SetTextureParametersCommand( editor, texture, parameters ) ); + + } catch ( e ) { + + // Edit cancelled + + } + + } + function onMapChange( texture ) { if ( texture !== null ) { @@ -124,6 +150,7 @@ function SidebarMaterialMapProperty( editor, property, name ) { } enabled.setDisabled( false ); + settings.setDisabled( texture === null ); onChange(); @@ -193,6 +220,7 @@ function SidebarMaterialMapProperty( editor, property, name ) { enabled.setValue( material[ property ] !== null ); enabled.setDisabled( map.getValue() === null ); + settings.setDisabled( map.getValue() === null ); if ( intensity !== undefined ) { diff --git a/editor/js/Strings.js b/editor/js/Strings.js index 5a116bb30c6812..c012af3d4aa7d5 100644 --- a/editor/js/Strings.js +++ b/editor/js/Strings.js @@ -35,6 +35,7 @@ function Strings( config ) { 'command/SetScene': 'تنظیم صحنه', 'command/SetScriptValue': 'تنظیم مقدار اسکریپت', 'command/SetShadowValue': 'تنظیم مقدار سایه', + 'command/SetTextureParameters': 'تنظیم پارامترهای تکسچر', 'command/SetUuid': 'تنظیم UUID', 'command/SetValue': 'تنظیم مقدار', @@ -427,6 +428,24 @@ function Strings( config ) { 'dialog/gltf/title': 'Import glTF', 'dialog/gltf/asScene': 'Import glTF as root scene', + 'dialog/texture/title': 'Texture Parameters', + 'dialog/texture/group/preview': 'Preview', + 'dialog/texture/group/mapping': 'Mapping', + 'dialog/texture/group/filtering': 'Filtering', + 'dialog/texture/group/transform': 'Transform', + 'dialog/texture/group/color': 'Color', + 'dialog/texture/mapping': 'Mapping', + 'dialog/texture/wrapS': 'Wrap S', + 'dialog/texture/wrapT': 'Wrap T', + 'dialog/texture/magFilter': 'Mag Filter', + 'dialog/texture/minFilter': 'Min Filter', + 'dialog/texture/anisotropy': 'Anisotropy', + 'dialog/texture/offset': 'Offset', + 'dialog/texture/repeat': 'Repeat', + 'dialog/texture/center': 'Center', + 'dialog/texture/rotation': 'Rotation', + 'dialog/texture/premultiplyAlpha': 'Premultiply Alpha', + 'dialog/texture/colorSpace': 'Color Space', 'dialog/ok': 'OK', 'dialog/cancel': 'Cancel' @@ -464,6 +483,7 @@ function Strings( config ) { 'command/SetScene': 'Set Scene', 'command/SetScriptValue': 'Set Script Value', 'command/SetShadowValue': 'Set Shadow Value', + 'command/SetTextureParameters': 'Set Texture Parameters', 'command/SetUuid': 'Set UUID', 'command/SetValue': 'Set Value', @@ -856,6 +876,24 @@ function Strings( config ) { 'dialog/gltf/title': 'Import glTF', 'dialog/gltf/asScene': 'Import glTF as root scene', + 'dialog/texture/title': 'Texture Parameters', + 'dialog/texture/group/preview': 'Preview', + 'dialog/texture/group/mapping': 'Mapping', + 'dialog/texture/group/filtering': 'Filtering', + 'dialog/texture/group/transform': 'Transform', + 'dialog/texture/group/color': 'Color', + 'dialog/texture/mapping': 'Mapping', + 'dialog/texture/wrapS': 'Wrap S', + 'dialog/texture/wrapT': 'Wrap T', + 'dialog/texture/magFilter': 'Mag Filter', + 'dialog/texture/minFilter': 'Min Filter', + 'dialog/texture/anisotropy': 'Anisotropy', + 'dialog/texture/offset': 'Offset', + 'dialog/texture/repeat': 'Repeat', + 'dialog/texture/center': 'Center', + 'dialog/texture/rotation': 'Rotation', + 'dialog/texture/premultiplyAlpha': 'Premultiply Alpha', + 'dialog/texture/colorSpace': 'Color Space', 'dialog/ok': 'OK', 'dialog/cancel': 'Cancel' @@ -894,6 +932,7 @@ function Strings( config ) { 'command/SetScene': 'Planter le décor', 'command/SetScriptValue': 'Définir la valeur du script', 'command/SetShadowValue': 'Set Shadow Value', + 'command/SetTextureParameters': 'Définir les paramètres de la texture', 'command/SetUuid': 'Définir l’UUID', 'command/SetValue': 'Définir la valeur', @@ -1286,6 +1325,24 @@ function Strings( config ) { 'dialog/gltf/title': 'Importer glTF', 'dialog/gltf/asScene': 'Importer glTF comme scène racine', + 'dialog/texture/title': 'Paramètres de la texture', + 'dialog/texture/group/preview': 'Aperçu', + 'dialog/texture/group/mapping': 'Mapping', + 'dialog/texture/group/filtering': 'Filtrage', + 'dialog/texture/group/transform': 'Transformation', + 'dialog/texture/group/color': 'Couleur', + 'dialog/texture/mapping': 'Mapping', + 'dialog/texture/wrapS': 'Wrap S', + 'dialog/texture/wrapT': 'Wrap T', + 'dialog/texture/magFilter': 'Filtre d’agrandissement', + 'dialog/texture/minFilter': 'Filtre de réduction', + 'dialog/texture/anisotropy': 'Anisotropie', + 'dialog/texture/offset': 'Décalage', + 'dialog/texture/repeat': 'Répétition', + 'dialog/texture/center': 'Centre', + 'dialog/texture/rotation': 'Rotation', + 'dialog/texture/premultiplyAlpha': 'Pré-multiplier alpha', + 'dialog/texture/colorSpace': 'Espace colorimétrique', 'dialog/ok': 'OK', 'dialog/cancel': 'Annuler' @@ -1324,6 +1381,7 @@ function Strings( config ) { 'command/SetScene': '设置布景', 'command/SetScriptValue': '设置脚本值', 'command/SetShadowValue': '设置阴影值', + 'command/SetTextureParameters': '设置纹理参数', 'command/SetUuid': '设置 UUID', 'command/SetValue': '设定值', @@ -1716,6 +1774,24 @@ function Strings( config ) { 'dialog/gltf/title': '导入 glTF', 'dialog/gltf/asScene': '将 glTF 导入为根场景', + 'dialog/texture/title': '纹理参数', + 'dialog/texture/group/preview': '预览', + 'dialog/texture/group/mapping': '映射', + 'dialog/texture/group/filtering': '过滤', + 'dialog/texture/group/transform': '变换', + 'dialog/texture/group/color': '颜色', + 'dialog/texture/mapping': '映射', + 'dialog/texture/wrapS': '环绕 S', + 'dialog/texture/wrapT': '环绕 T', + 'dialog/texture/magFilter': '放大过滤器', + 'dialog/texture/minFilter': '缩小过滤器', + 'dialog/texture/anisotropy': '各向异性', + 'dialog/texture/offset': '偏移', + 'dialog/texture/repeat': '重复', + 'dialog/texture/center': '中心', + 'dialog/texture/rotation': '旋转', + 'dialog/texture/premultiplyAlpha': '预乘 Alpha', + 'dialog/texture/colorSpace': '颜色空间', 'dialog/ok': '确定', 'dialog/cancel': '取消' @@ -1754,6 +1830,7 @@ function Strings( config ) { 'command/SetScene': 'セットシーン', 'command/SetScriptValue': 'スクリプト値の設定', 'command/SetShadowValue': 'Set Shadow Value', + 'command/SetTextureParameters': 'テクスチャパラメータの設定', 'command/SetUuid': 'UUIDの設定', 'command/SetValue': '値の設定', @@ -2146,6 +2223,24 @@ function Strings( config ) { 'dialog/gltf/title': 'glTFをインポート', 'dialog/gltf/asScene': 'glTFをルートシーンとしてインポート', + 'dialog/texture/title': 'テクスチャパラメータ', + 'dialog/texture/group/preview': 'プレビュー', + 'dialog/texture/group/mapping': 'マッピング', + 'dialog/texture/group/filtering': 'フィルタリング', + 'dialog/texture/group/transform': '変換', + 'dialog/texture/group/color': 'カラー', + 'dialog/texture/mapping': 'マッピング', + 'dialog/texture/wrapS': 'ラップ S', + 'dialog/texture/wrapT': 'ラップ T', + 'dialog/texture/magFilter': '拡大フィルター', + 'dialog/texture/minFilter': '縮小フィルター', + 'dialog/texture/anisotropy': '異方性', + 'dialog/texture/offset': 'オフセット', + 'dialog/texture/repeat': 'リピート', + 'dialog/texture/center': '中心', + 'dialog/texture/rotation': '回転', + 'dialog/texture/premultiplyAlpha': 'プリマルチプライアルファ', + 'dialog/texture/colorSpace': '色空間', 'dialog/ok': 'OK', 'dialog/cancel': 'キャンセル' @@ -2183,6 +2278,7 @@ function Strings( config ) { 'command/SetScene': '장면 설정', 'command/SetScriptValue': '스크립트 값 설정', 'command/SetShadowValue': '그림자 값 설정', + 'command/SetTextureParameters': '텍스처 매개변수 설정', 'command/SetUuid': 'UUID 설정', 'command/SetValue': '값 설정', @@ -2575,6 +2671,24 @@ function Strings( config ) { 'dialog/gltf/title': 'glTF 가져오기', 'dialog/gltf/asScene': 'glTF를 루트 씬으로 가져오기', + 'dialog/texture/title': '텍스처 매개변수', + 'dialog/texture/group/preview': '미리보기', + 'dialog/texture/group/mapping': '매핑', + 'dialog/texture/group/filtering': '필터링', + 'dialog/texture/group/transform': '변환', + 'dialog/texture/group/color': '색상', + 'dialog/texture/mapping': '매핑', + 'dialog/texture/wrapS': '랩 S', + 'dialog/texture/wrapT': '랩 T', + 'dialog/texture/magFilter': '확대 필터', + 'dialog/texture/minFilter': '축소 필터', + 'dialog/texture/anisotropy': '이방성', + 'dialog/texture/offset': '오프셋', + 'dialog/texture/repeat': '반복', + 'dialog/texture/center': '중심', + 'dialog/texture/rotation': '회전', + 'dialog/texture/premultiplyAlpha': '알파 미리 곱하기', + 'dialog/texture/colorSpace': '색 공간', 'dialog/ok': '확인', 'dialog/cancel': '취소' } diff --git a/editor/js/TextureParametersDialog.js b/editor/js/TextureParametersDialog.js new file mode 100644 index 00000000000000..a911345c075de6 --- /dev/null +++ b/editor/js/TextureParametersDialog.js @@ -0,0 +1,293 @@ +import * as THREE from 'three'; + +import { UIButton, UICheckbox, UIDiv, UINumber, UIRow, UISelect, UIText } from './libs/ui.js'; +import { renderToCanvas } from './libs/ui.three.js'; + +class TextureParametersDialog { + + constructor( editor, texture ) { + + this.editor = editor; + + this.strings = editor.strings; + this.texture = texture; + + const dom = new UIDiv(); + dom.setClass( 'Dialog' ); + this.dom = dom.dom; + + const background = new UIDiv(); + background.setClass( 'Dialog-background' ); + background.dom.addEventListener( 'click', () => this.cancel() ); + dom.add( background ); + + const content = new UIDiv(); + content.setClass( 'Dialog-content TextureParametersDialog-content' ); + dom.add( content ); + + // Title + + const titleBar = new UIDiv(); + titleBar.setClass( 'Dialog-title' ); + titleBar.setTextContent( this.strings.getKey( 'dialog/texture/title' ) ); + content.add( titleBar ); + + // Body (split into preview + form) + + const body = new UIDiv(); + body.setClass( 'Dialog-body TextureParametersDialog-body' ); + content.add( body ); + + const split = new UIDiv(); + split.setClass( 'TextureParametersDialog-split' ); + body.add( split ); + + // Preview + + const previewWrapper = new UIDiv(); + previewWrapper.setClass( 'TextureParametersDialog-preview' ); + split.add( previewWrapper ); + + previewWrapper.add( createGroupHeading( this.strings.getKey( 'dialog/texture/group/preview' ) ) ); + + const previewCanvas = document.createElement( 'canvas' ); + previewCanvas.width = 400; + previewCanvas.height = 400; + previewWrapper.dom.appendChild( previewCanvas ); + + this.previewCanvas = previewCanvas; + this.previewContext = previewCanvas.getContext( '2d' ); + this.previewTexture = texture.clone(); + + const updatePreview = () => this.updatePreview(); + + // Form + + const form = new UIDiv(); + form.setClass( 'TextureParametersDialog-form' ); + split.add( form ); + + // Mapping group + + form.add( createGroupHeading( this.strings.getKey( 'dialog/texture/group/mapping' ) ) ); + + this.mapping = new UISelect().setOptions( { + [ THREE.UVMapping ]: 'UV', + [ THREE.EquirectangularReflectionMapping ]: 'Equirectangular Reflection', + [ THREE.EquirectangularRefractionMapping ]: 'Equirectangular Refraction', + [ THREE.CubeReflectionMapping ]: 'Cube Reflection', + [ THREE.CubeRefractionMapping ]: 'Cube Refraction', + [ THREE.CubeUVReflectionMapping ]: 'CubeUV Reflection' + } ).setValue( texture.mapping ).onChange( updatePreview ); + form.add( createRow( this.strings.getKey( 'dialog/texture/mapping' ), this.mapping ) ); + + const wrapOptions = { + [ THREE.RepeatWrapping ]: 'Repeat', + [ THREE.ClampToEdgeWrapping ]: 'Clamp To Edge', + [ THREE.MirroredRepeatWrapping ]: 'Mirrored Repeat' + }; + + this.wrapS = new UISelect().setOptions( wrapOptions ).setValue( texture.wrapS ).onChange( updatePreview ); + form.add( createRow( this.strings.getKey( 'dialog/texture/wrapS' ), this.wrapS ) ); + + this.wrapT = new UISelect().setOptions( wrapOptions ).setValue( texture.wrapT ).onChange( updatePreview ); + form.add( createRow( this.strings.getKey( 'dialog/texture/wrapT' ), this.wrapT ) ); + + // Filtering group + + form.add( createGroupHeading( this.strings.getKey( 'dialog/texture/group/filtering' ) ) ); + + this.minFilter = new UISelect().setOptions( { + [ THREE.NearestFilter ]: 'Nearest', + [ THREE.NearestMipmapNearestFilter ]: 'Nearest Mipmap Nearest', + [ THREE.NearestMipmapLinearFilter ]: 'Nearest Mipmap Linear', + [ THREE.LinearFilter ]: 'Linear', + [ THREE.LinearMipmapNearestFilter ]: 'Linear Mipmap Nearest', + [ THREE.LinearMipmapLinearFilter ]: 'Linear Mipmap Linear' + } ).setValue( texture.minFilter ).onChange( updatePreview ); + form.add( createRow( this.strings.getKey( 'dialog/texture/minFilter' ), this.minFilter ) ); + + this.magFilter = new UISelect().setOptions( { + [ THREE.NearestFilter ]: 'Nearest', + [ THREE.LinearFilter ]: 'Linear' + } ).setValue( texture.magFilter ).onChange( updatePreview ); + form.add( createRow( this.strings.getKey( 'dialog/texture/magFilter' ), this.magFilter ) ); + + this.anisotropy = new UINumber( texture.anisotropy ).setPrecision( 0 ).setRange( 1, 16 ).setNudge( 1 ).setStep( 1 ).setWidth( '60px' ).onChange( updatePreview ); + form.add( createRow( this.strings.getKey( 'dialog/texture/anisotropy' ), this.anisotropy ) ); + + // Transform group + + form.add( createGroupHeading( this.strings.getKey( 'dialog/texture/group/transform' ) ) ); + + this.offsetX = new UINumber( texture.offset.x ).setWidth( '60px' ).onChange( updatePreview ); + this.offsetY = new UINumber( texture.offset.y ).setWidth( '60px' ).onChange( updatePreview ); + form.add( createRow( this.strings.getKey( 'dialog/texture/offset' ), this.offsetX, this.offsetY ) ); + + this.repeatX = new UINumber( texture.repeat.x ).setWidth( '60px' ).onChange( updatePreview ); + this.repeatY = new UINumber( texture.repeat.y ).setWidth( '60px' ).onChange( updatePreview ); + form.add( createRow( this.strings.getKey( 'dialog/texture/repeat' ), this.repeatX, this.repeatY ) ); + + this.centerX = new UINumber( texture.center.x ).setWidth( '60px' ).onChange( updatePreview ); + this.centerY = new UINumber( texture.center.y ).setWidth( '60px' ).onChange( updatePreview ); + form.add( createRow( this.strings.getKey( 'dialog/texture/center' ), this.centerX, this.centerY ) ); + + this.rotation = new UINumber( texture.rotation * THREE.MathUtils.RAD2DEG ).setStep( 10 ).setNudge( 0.1 ).setUnit( '°' ).setWidth( '60px' ).onChange( updatePreview ); + form.add( createRow( this.strings.getKey( 'dialog/texture/rotation' ), this.rotation ) ); + + // Color group + + form.add( createGroupHeading( this.strings.getKey( 'dialog/texture/group/color' ) ) ); + + this.premultiplyAlpha = new UICheckbox( texture.premultiplyAlpha ).onChange( updatePreview ); + form.add( createRow( this.strings.getKey( 'dialog/texture/premultiplyAlpha' ), this.premultiplyAlpha ) ); + + this.colorSpace = new UISelect().setOptions( { + [ THREE.NoColorSpace ]: 'No Color Space', + [ THREE.SRGBColorSpace ]: 'sRGB', + [ THREE.LinearSRGBColorSpace ]: 'Linear sRGB' + } ).setValue( texture.colorSpace ).onChange( updatePreview ); + form.add( createRow( this.strings.getKey( 'dialog/texture/colorSpace' ), this.colorSpace ) ); + + updatePreview(); + + // Buttons + + const buttonsRow = new UIDiv(); + buttonsRow.setClass( 'Dialog-buttons' ); + body.add( buttonsRow ); + + const okButton = new UIButton( this.strings.getKey( 'dialog/ok' ) ); + okButton.setWidth( '80px' ); + okButton.onClick( () => this.confirm() ); + buttonsRow.add( okButton ); + + const cancelButton = new UIButton( this.strings.getKey( 'dialog/cancel' ) ); + cancelButton.setWidth( '80px' ); + cancelButton.setMarginLeft( '8px' ); + cancelButton.onClick( () => this.cancel() ); + buttonsRow.add( cancelButton ); + + // Promise handlers + + this.resolve = null; + this.reject = null; + + } + + show() { + + document.body.appendChild( this.dom ); + + return new Promise( ( resolve, reject ) => { + + this.resolve = resolve; + this.reject = reject; + + } ); + + } + + getCurrentParameters() { + + return { + mapping: parseInt( this.mapping.getValue() ), + wrapS: parseInt( this.wrapS.getValue() ), + wrapT: parseInt( this.wrapT.getValue() ), + magFilter: parseInt( this.magFilter.getValue() ), + minFilter: parseInt( this.minFilter.getValue() ), + anisotropy: this.anisotropy.getValue(), + offset: { x: this.offsetX.getValue(), y: this.offsetY.getValue() }, + repeat: { x: this.repeatX.getValue(), y: this.repeatY.getValue() }, + center: { x: this.centerX.getValue(), y: this.centerY.getValue() }, + rotation: this.rotation.getValue() * THREE.MathUtils.DEG2RAD, + premultiplyAlpha: this.premultiplyAlpha.getValue(), + colorSpace: this.colorSpace.getValue() + }; + + } + + updatePreview() { + + applyParameters( this.previewTexture, this.getCurrentParameters() ); + + const rendered = renderToCanvas( this.previewTexture ); + + const canvas = this.previewCanvas; + const context = this.previewContext; + context.clearRect( 0, 0, canvas.width, canvas.height ); + + if ( rendered.width === 0 || rendered.height === 0 ) return; + + const scale = Math.min( canvas.width / rendered.width, canvas.height / rendered.height ); + const w = rendered.width * scale; + const h = rendered.height * scale; + context.drawImage( rendered, ( canvas.width - w ) / 2, ( canvas.height - h ) / 2, w, h ); + + } + + confirm() { + + const result = this.getCurrentParameters(); + + this.previewTexture.dispose(); + this.dom.remove(); + + if ( this.resolve ) this.resolve( result ); + + } + + cancel() { + + this.previewTexture.dispose(); + this.dom.remove(); + + if ( this.reject ) this.reject( new Error( 'Texture parameters edit cancelled' ) ); + + } + +} + +function createRow( label, ...inputs ) { + + const row = new UIRow(); + row.add( new UIText( label ).setClass( 'Label' ) ); + + for ( const input of inputs ) { + + row.add( input ); + + } + + return row; + +} + +function createGroupHeading( label ) { + + const heading = new UIText( label ); + heading.setClass( 'TextureParametersDialog-groupHeading' ); + heading.setStyle( 'display', [ 'block' ] ); + return heading; + +} + +function applyParameters( texture, p ) { + + texture.mapping = p.mapping; + texture.wrapS = p.wrapS; + texture.wrapT = p.wrapT; + texture.magFilter = p.magFilter; + texture.minFilter = p.minFilter; + texture.anisotropy = p.anisotropy; + texture.offset.set( p.offset.x, p.offset.y ); + texture.repeat.set( p.repeat.x, p.repeat.y ); + texture.center.set( p.center.x, p.center.y ); + texture.rotation = p.rotation; + texture.premultiplyAlpha = p.premultiplyAlpha; + texture.colorSpace = p.colorSpace; + texture.needsUpdate = true; + +} + +export { TextureParametersDialog }; diff --git a/editor/js/commands/Commands.js b/editor/js/commands/Commands.js index 78bc3fc64335cf..0e04c613da58ea 100644 --- a/editor/js/commands/Commands.js +++ b/editor/js/commands/Commands.js @@ -19,5 +19,6 @@ export { SetScaleCommand } from './SetScaleCommand.js'; export { SetSceneCommand } from './SetSceneCommand.js'; export { SetScriptValueCommand } from './SetScriptValueCommand.js'; export { SetShadowValueCommand } from './SetShadowValueCommand.js'; +export { SetTextureParametersCommand } from './SetTextureParametersCommand.js'; export { SetUuidCommand } from './SetUuidCommand.js'; export { SetValueCommand } from './SetValueCommand.js'; diff --git a/editor/js/commands/SetTextureParametersCommand.js b/editor/js/commands/SetTextureParametersCommand.js new file mode 100644 index 00000000000000..18e985a7aa8b62 --- /dev/null +++ b/editor/js/commands/SetTextureParametersCommand.js @@ -0,0 +1,143 @@ +import { Command } from '../Command.js'; + +const VECTOR_KEYS = [ 'offset', 'repeat', 'center' ]; + +class SetTextureParametersCommand extends Command { + + /** + * @param {Editor} editor + * @param {THREE.Texture} texture + * @param {Object} newParameters + * @constructor + */ + constructor( editor, texture = null, newParameters = {} ) { + + super( editor ); + + this.type = 'SetTextureParametersCommand'; + this.name = editor.strings.getKey( 'command/SetTextureParameters' ); + + this.texture = texture; + + this.oldParameters = ( texture !== null ) ? extractParameters( texture, newParameters ) : {}; + this.newParameters = newParameters; + + } + + execute() { + + applyParameters( this.texture, this.newParameters ); + this.editor.signals.sceneGraphChanged.dispatch(); + + } + + undo() { + + applyParameters( this.texture, this.oldParameters ); + this.editor.signals.sceneGraphChanged.dispatch(); + + } + + toJSON() { + + const output = super.toJSON( this ); + + output.textureUuid = this.texture.uuid; + output.oldParameters = this.oldParameters; + output.newParameters = this.newParameters; + + return output; + + } + + fromJSON( json ) { + + super.fromJSON( json ); + + this.texture = findTextureByUuid( this.editor, json.textureUuid ); + this.oldParameters = json.oldParameters; + this.newParameters = json.newParameters; + + } + +} + +function extractParameters( texture, reference ) { + + const result = {}; + + for ( const key in reference ) { + + const value = texture[ key ]; + + if ( VECTOR_KEYS.includes( key ) ) { + + result[ key ] = { x: value.x, y: value.y }; + + } else { + + result[ key ] = value; + + } + + } + + return result; + +} + +function applyParameters( texture, parameters ) { + + for ( const key in parameters ) { + + const value = parameters[ key ]; + + if ( VECTOR_KEYS.includes( key ) ) { + + texture[ key ].set( value.x, value.y ); + + } else { + + texture[ key ] = value; + + } + + } + + texture.needsUpdate = true; + +} + +function findTextureByUuid( editor, uuid ) { + + let result = null; + + editor.scene.traverse( ( object ) => { + + if ( object.material === undefined ) return; + + const materials = Array.isArray( object.material ) ? object.material : [ object.material ]; + + for ( const material of materials ) { + + for ( const key in material ) { + + const value = material[ key ]; + + if ( value && value.isTexture === true && value.uuid === uuid ) { + + result = value; + + } + + } + + } + + } ); + + return result; + +} + +export { SetTextureParametersCommand }; diff --git a/editor/js/libs/ui.three.js b/editor/js/libs/ui.three.js index 40fdd7c4bf38a0..0ac5490810715c 100644 --- a/editor/js/libs/ui.three.js +++ b/editor/js/libs/ui.three.js @@ -53,13 +53,23 @@ class UITexture extends UISpan { const hash = `${file.lastModified}_${file.size}_${file.name}`; - if ( cache.has( hash ) ) { + function deliver( texture ) { + + if ( ! cache.has( hash ) ) cache.set( hash, texture ); - const texture = cache.get( hash ); + const cached = cache.get( hash ); + const clone = cached.clone(); + clone.sourceFile = cached.sourceFile; - scope.setValue( texture ); + scope.setValue( clone ); - if ( scope.onChangeCallback ) scope.onChangeCallback( texture ); + if ( scope.onChangeCallback ) scope.onChangeCallback( clone ); + + } + + if ( cache.has( hash ) ) { + + deliver( cache.get( hash ) ); } else if ( extension === 'hdr' || extension === 'pic' ) { @@ -74,11 +84,7 @@ class UITexture extends UISpan { hdrTexture.sourceFile = file.name; - cache.set( hash, hdrTexture ); - - scope.setValue( hdrTexture ); - - if ( scope.onChangeCallback ) scope.onChangeCallback( hdrTexture ); + deliver( hdrTexture ); } ); @@ -98,12 +104,7 @@ class UITexture extends UISpan { texture.colorSpace = THREE.SRGBColorSpace; texture.sourceFile = file.name; - cache.set( hash, texture ); - - scope.setValue( texture ); - - if ( scope.onChangeCallback ) scope.onChangeCallback( texture ); - + deliver( texture ); } ); @@ -129,11 +130,7 @@ class UITexture extends UISpan { texture.sourceFile = file.name; texture.needsUpdate = true; - cache.set( hash, texture ); - - scope.setValue( texture ); - - if ( scope.onChangeCallback ) scope.onChangeCallback( texture ); + deliver( texture ); ktx2Loader.dispose(); } ); @@ -157,11 +154,7 @@ class UITexture extends UISpan { texture.sourceFile = file.name; texture.needsUpdate = true; - cache.set( hash, texture ); - - scope.setValue( texture ); - - if ( scope.onChangeCallback ) scope.onChangeCallback( texture ); + deliver( texture ); } ); @@ -180,11 +173,7 @@ class UITexture extends UISpan { texture.sourceFile = file.name; texture.needsUpdate = true; - cache.set( hash, texture ); - - scope.setValue( texture ); - - if ( scope.onChangeCallback ) scope.onChangeCallback( texture ); + deliver( texture ); }, false ); @@ -882,4 +871,4 @@ function renderToCanvas( texture ) { } -export { UITexture, UIOutliner, UIPoints, UIPoints2, UIPoints3, UIBoolean }; +export { UITexture, UIOutliner, UIPoints, UIPoints2, UIPoints3, UIBoolean, renderToCanvas }; diff --git a/examples/jsm/tsl/display/GTAONode.js b/examples/jsm/tsl/display/GTAONode.js index 50e5f242e56727..a0f07953c2b787 100644 --- a/examples/jsm/tsl/display/GTAONode.js +++ b/examples/jsm/tsl/display/GTAONode.js @@ -1,5 +1,5 @@ import { DataTexture, RenderTarget, RepeatWrapping, Vector2, Vector3, TempNode, QuadMesh, NodeMaterial, RendererUtils, RedFormat } from 'three/webgpu'; -import { reference, logarithmicDepthToViewZ, viewZToPerspectiveDepth, getNormalFromDepth, getScreenPosition, getViewPosition, nodeObject, Fn, float, NodeUpdateType, uv, uniform, Loop, vec2, vec3, vec4, int, dot, max, pow, abs, If, textureSize, sin, cos, PI, texture, passTexture, mat3, add, normalize, mul, cross, div, mix, sqrt, sub, acos, clamp } from 'three/tsl'; +import { reference, logarithmicDepthToViewZ, viewZToPerspectiveDepth, getNormalFromDepth, getScreenPosition, getViewPosition, nodeObject, Fn, float, NodeUpdateType, uv, uniform, Loop, vec2, vec3, vec4, int, dot, max, pow, abs, If, textureSize, sin, cos, PI, texture, passTexture, mat3, add, normalize, mul, cross, div, mix, acos, clamp } from 'three/tsl'; const _quadMesh = /*@__PURE__*/ new QuadMesh(); const _size = /*@__PURE__*/ new Vector2(); @@ -375,10 +375,22 @@ class GTAONode extends TempNode { const viewDir = normalize( viewPosition.xyz.negate() ).toVar(); const sliceBitangent = normalize( cross( sampleDir.xyz, viewDir ) ).toVar(); - const sliceTangent = cross( sliceBitangent, viewDir ); - const normalInSlice = normalize( viewNormal.sub( sliceBitangent.mul( dot( viewNormal, sliceBitangent ) ) ) ); - - const tangentToNormalInSlice = cross( normalInSlice, sliceBitangent ).toVar(); + const sliceTangent = cross( sliceBitangent, viewDir ).toVar(); + + // Project the view normal onto the slice plane (remove component along sliceBitangent). + // The unnormalized length is the foreshortening weight applied at slice integration. + // (Activision GTAO paper, Section 3.2 "Per-pixel sampling".) + const projNRaw = viewNormal.sub( sliceBitangent.mul( dot( viewNormal, sliceBitangent ) ) ).toVar(); + const projNLen = projNRaw.length().toVar(); + const projN = projNRaw.div( max( projNLen, float( 0.0001 ) ) ).toVar(); + + // γ — angle of projN within the slice plane, signed by the tangent direction. + const nSin = dot( projN, sliceTangent ).toVar(); + const nCos = clamp( dot( projN, viewDir ), 0, 1 ).toVar(); + const signNSin = nSin.greaterThanEqual( 0 ).select( float( 1 ), float( - 1 ) ); + const angleN = signNSin.mul( acos( nCos ) ).toVar(); + + const tangentToNormalInSlice = cross( projN, sliceBitangent ).toVar(); const cosHorizons = vec2( dot( viewDir, tangentToNormalInSlice ), dot( viewDir, tangentToNormalInSlice.negate() ) ).toVar(); // For each slice, the inner loop performs ray marching to find the horizons. @@ -419,15 +431,24 @@ class GTAONode extends TempNode { } ); - // After the horizons are found for a given slice, their contribution to the total occlusion is calculated. + // Cosine-weighted inner integral, closed-form (Activision GTAO paper, Eq. 7). + // Per horizon h_i: term_i = −cos( 2 h_i − γ ) + cos( γ ) + 2 h_i sin( γ ) + // The 0.25 factor is ½ (integral normalization) × ½ (averaging the two horizons). + // + // In this slice setup `sliceTangent = cross( sliceBitangent, viewDir )` works out + // opposite to `sampleDir`, so the +sampleDir samples (cosHorizons.x) live on the + // −T side of the slice and −sampleDir samples (cosHorizons.y) on the +T side. + // γ is signed by +T (sliceTangent), so hPos must read from cosHorizons.y. + + const hPos = acos( cosHorizons.y ).toVar(); + const hNeg = acos( cosHorizons.x ).negate().toVar(); + + const termPos = cos( hPos.mul( 2 ).sub( angleN ) ).negate().add( nCos ).add( hPos.mul( 2 ).mul( nSin ) ); + const termNeg = cos( hNeg.mul( 2 ).sub( angleN ) ).negate().add( nCos ).add( hNeg.mul( 2 ).mul( nSin ) ); + const a = termPos.add( termNeg ).mul( 0.25 ); - const sinHorizons = sqrt( sub( 1.0, cosHorizons.mul( cosHorizons ) ) ).toVar(); - const nx = dot( normalInSlice, sliceTangent ); - const ny = dot( normalInSlice, viewDir ); - const nxb = mul( 0.5, acos( cosHorizons.y ).sub( acos( cosHorizons.x ) ).add( sinHorizons.x.mul( cosHorizons.x ).sub( sinHorizons.y.mul( cosHorizons.y ) ) ) ); - const nyb = mul( 0.5, sub( 2.0, cosHorizons.x.mul( cosHorizons.x ) ).sub( cosHorizons.y.mul( cosHorizons.y ) ) ); - const occlusion = nx.mul( nxb ).add( ny.mul( nyb ) ); - ao.addAssign( occlusion ); + // |projN| is the foreshortening weight from the per-slice normal projection. + ao.addAssign( projNLen.mul( a ) ); } ); diff --git a/examples/models/gltf/tennyson-bust.glb b/examples/models/gltf/tennyson-bust.glb index 25020b2d47e931..6be2cd30df1bf1 100644 Binary files a/examples/models/gltf/tennyson-bust.glb and b/examples/models/gltf/tennyson-bust.glb differ diff --git a/examples/webgpu_postprocessing_ao.html b/examples/webgpu_postprocessing_ao.html index d01d40a7979919..118b15e301b098 100644 --- a/examples/webgpu_postprocessing_ao.html +++ b/examples/webgpu_postprocessing_ao.html @@ -488,7 +488,7 @@ const fitBox = new THREE.Box3().setFromObject( bust ); const center = new THREE.Vector3(); fitBox.getCenter( center ); - bust.position.set( bustX - center.x, - 0.7 - fitBox.min.y, bustZ - center.z ); + bust.position.set( bustX - center.x, - 0.7 - fitBox.min.y, bustZ - center.z + 0.1 ); scene.add( bust ); // events