diff --git a/spine-ts/spine-threejs/example/shadow.html b/spine-ts/spine-threejs/example/shadow.html new file mode 100644 index 0000000000..d48ae92570 --- /dev/null +++ b/spine-ts/spine-threejs/example/shadow.html @@ -0,0 +1,244 @@ + + + + spine-threejs + + + + + + + + + + diff --git a/spine-ts/spine-threejs/src/MeshBatcher.ts b/spine-ts/spine-threejs/src/MeshBatcher.ts index ac6fc1114b..4f5604025c 100644 --- a/spine-ts/spine-threejs/src/MeshBatcher.ts +++ b/spine-ts/spine-threejs/src/MeshBatcher.ts @@ -27,12 +27,16 @@ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import { SkeletonMeshMaterial, SkeletonMeshMaterialParametersCustomizer } from "./SkeletonMesh.js"; import * as THREE from "three" + import { ThreeJsTexture, ThreeBlendOptions } from "./ThreeJsTexture.js"; import { BlendMode } from "@esotericsoftware/spine-core"; +import { SkeletonMesh } from "./SkeletonMesh.js"; +type MaterialWithMap = THREE.Material & { map: THREE.Texture | null }; export class MeshBatcher extends THREE.Mesh { + public static MAX_VERTICES = 10920; + private static VERTEX_SIZE = 9; private vertexBuffer: THREE.InterleavedBuffer; private vertices: Float32Array; @@ -41,9 +45,9 @@ export class MeshBatcher extends THREE.Mesh { private indicesLength = 0; private materialGroups: [number, number, number][] = []; - constructor (maxVertices: number = 10920, private materialCustomizer: SkeletonMeshMaterialParametersCustomizer = (parameters) => { }) { + constructor (maxVertices: number = MeshBatcher.MAX_VERTICES, private materialFactory: (parameters: THREE.MaterialParameters) => THREE.Material) { super(); - if (maxVertices > 10920) throw new Error("Can't have more than 10920 triangles per batch: " + maxVertices); + if (maxVertices > MeshBatcher.MAX_VERTICES) throw new Error("Can't have more than 10920 triangles per batch: " + maxVertices); let vertices = this.vertices = new Float32Array(maxVertices * MeshBatcher.VERTEX_SIZE); let indices = this.indices = new Uint16Array(maxVertices * 3); let geo = new THREE.BufferGeometry(); @@ -57,7 +61,7 @@ export class MeshBatcher extends THREE.Mesh { geo.drawRange.start = 0; geo.drawRange.count = 0; this.geometry = geo; - this.material = [new SkeletonMeshMaterial(materialCustomizer)]; + this.material = []; } dispose () { @@ -80,13 +84,13 @@ export class MeshBatcher extends THREE.Mesh { geo.clearGroups(); this.materialGroups = []; if (this.material instanceof THREE.Material) { - const meshMaterial = this.material as SkeletonMeshMaterial; - meshMaterial.uniforms.map.value = null; + const meshMaterial = this.material as MaterialWithMap; + meshMaterial.map = null; meshMaterial.blending = THREE.NormalBlending; } else if (Array.isArray(this.material)) { for (let i = 0; i < this.material.length; i++) { - const meshMaterial = this.material[i] as SkeletonMeshMaterial; - meshMaterial.uniforms.map.value = null; + const meshMaterial = this.material[i] as MaterialWithMap; + meshMaterial.map = null; meshMaterial.blending = THREE.NormalBlending; } } @@ -167,14 +171,14 @@ export class MeshBatcher extends THREE.Mesh { if (Array.isArray(this.material)) { for (let i = 0; i < this.material.length; i++) { - const meshMaterial = this.material[i] as SkeletonMeshMaterial; + const meshMaterial = this.material[i] as MaterialWithMap; - if (!meshMaterial.uniforms.map.value) { + if (!meshMaterial.map) { updateMeshMaterial(meshMaterial, slotTexture, blendingObject); return i; } - if (meshMaterial.uniforms.map.value === slotTexture + if (meshMaterial.map === slotTexture && blendingObject.blending === meshMaterial.blending && (blendingObject.blendSrc === undefined || blendingObject.blendSrc === meshMaterial.blendSrc) && (blendingObject.blendDst === undefined || blendingObject.blendDst === meshMaterial.blendDst) @@ -185,8 +189,13 @@ export class MeshBatcher extends THREE.Mesh { } } - const meshMaterial = new SkeletonMeshMaterial(this.materialCustomizer); - updateMeshMaterial(meshMaterial, slotTexture, blendingObject); + const meshMaterial = this.materialFactory(SkeletonMesh.DEFAULT_MATERIAL_PARAMETERS); + + if (!('map' in meshMaterial)) { + throw new Error("The material factory must return a material having the map property for the texture."); + } + + updateMeshMaterial(meshMaterial as MaterialWithMap, slotTexture, blendingObject); this.material.push(meshMaterial); group = this.material.length - 1; } else { @@ -197,8 +206,8 @@ export class MeshBatcher extends THREE.Mesh { } } -function updateMeshMaterial (meshMaterial: SkeletonMeshMaterial, slotTexture: THREE.Texture, blending: ThreeBlendOptions) { - meshMaterial.uniforms.map.value = slotTexture; +function updateMeshMaterial (meshMaterial: MaterialWithMap, slotTexture: THREE.Texture, blending: ThreeBlendOptions) { + meshMaterial.map = slotTexture; Object.assign(meshMaterial, blending); meshMaterial.needsUpdate = true; } diff --git a/spine-ts/spine-threejs/src/SkeletonMesh.ts b/spine-ts/spine-threejs/src/SkeletonMesh.ts index 71b99d3052..d48efb4039 100644 --- a/spine-ts/spine-threejs/src/SkeletonMesh.ts +++ b/spine-ts/spine-threejs/src/SkeletonMesh.ts @@ -27,10 +27,10 @@ * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ +import * as THREE from "three"; import { AnimationState, AnimationStateData, - BlendMode, ClippingAttachment, Color, MeshAttachment, @@ -40,69 +40,26 @@ import { Skeleton, SkeletonClipping, SkeletonData, - TextureAtlasRegion, Utils, Vector2, } from "@esotericsoftware/spine-core"; + import { MeshBatcher } from "./MeshBatcher.js"; -import * as THREE from "three"; import { ThreeJsTexture } from "./ThreeJsTexture.js"; +import { Material } from "three"; -export type SkeletonMeshMaterialParametersCustomizer = ( - materialParameters: THREE.ShaderMaterialParameters -) => void; - -export class SkeletonMeshMaterial extends THREE.ShaderMaterial { - constructor (customizer: SkeletonMeshMaterialParametersCustomizer) { - let vertexShader = ` - attribute vec4 color; - varying vec2 vUv; - varying vec4 vColor; - void main() { - vUv = uv; - vColor = color; - gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0); - } - `; - let fragmentShader = ` - uniform sampler2D map; - #ifdef USE_SPINE_ALPHATEST - uniform float alphaTest; - #endif - varying vec2 vUv; - varying vec4 vColor; - void main(void) { - gl_FragColor = texture2D(map, vUv)*vColor; - #ifdef USE_SPINE_ALPHATEST - if (gl_FragColor.a < alphaTest) discard; - #endif - } - `; +type SkeletonMeshMaterialParametersCustomizer = (materialParameters: THREE.MaterialParameters) => void; - let parameters: THREE.ShaderMaterialParameters = { - uniforms: { - map: { value: null }, - }, - vertexShader: vertexShader, - fragmentShader: fragmentShader, - side: THREE.DoubleSide, - transparent: true, - depthWrite: true, - alphaTest: 0.0, - }; - customizer(parameters); - if (parameters.alphaTest && parameters.alphaTest > 0) { - parameters.defines = { USE_SPINE_ALPHATEST: 1 }; - if (!parameters.uniforms) parameters.uniforms = {}; - parameters.uniforms["alphaTest"] = { value: parameters.alphaTest }; - } - super(parameters); - // non-pma textures are premultiply on upload, so we set premultipliedAlpha to true - this.premultipliedAlpha = true; +export class SkeletonMesh extends THREE.Object3D { + public static readonly DEFAULT_MATERIAL_PARAMETERS: THREE.MaterialParameters = { + side: THREE.DoubleSide, + transparent: true, + depthWrite: true, + alphaTest: 0.0, + premultipliedAlpha: true, + vertexColors: true, } -} -export class SkeletonMesh extends THREE.Object3D { tempPos: Vector2 = new Vector2(); tempUv: Vector2 = new Vector2(); tempLight = new Color(); @@ -112,6 +69,7 @@ export class SkeletonMesh extends THREE.Object3D { zOffset: number = 0.1; private batches = new Array(); + private materialFactory: (parameters: THREE.MaterialParameters) => Material; private nextBatchIndex = 0; private clipper: SkeletonClipping = new SkeletonClipping(); @@ -121,17 +79,67 @@ export class SkeletonMesh extends THREE.Object3D { private vertices = Utils.newFloatArray(1024); private tempColor = new Color(); + private _castShadow = false; + private _receiveShadow = false; + + constructor (configuration: { skeletonData: SkeletonData, materialFactory?: (parameters: THREE.MaterialParameters) => Material }) + /** + * @deprecated TODO + * + * @param skeletonData + * @param materialCustomizer + */ constructor ( skeletonData: SkeletonData, - private materialCustomerizer: SkeletonMeshMaterialParametersCustomizer = ( - material - ) => { } + materialCustomizer: SkeletonMeshMaterialParametersCustomizer, + ) + constructor ( + skeletonDataOrConfiguration: SkeletonData | { skeletonData: SkeletonData, materialFactory?: (parameters: THREE.MaterialParameters) => Material }, + materialCustomizer: SkeletonMeshMaterialParametersCustomizer = () => { } ) { super(); - this.skeleton = new Skeleton(skeletonData); - let animData = new AnimationStateData(skeletonData); + if (!('skeletonData' in skeletonDataOrConfiguration)) { + const materialFactory = () => { + const parameters: THREE.MaterialParameters = { ...SkeletonMesh.DEFAULT_MATERIAL_PARAMETERS }; + materialCustomizer(parameters); + return new THREE.MeshBasicMaterial(parameters); + }; + skeletonDataOrConfiguration = { + skeletonData: skeletonDataOrConfiguration, + materialFactory, + } + } + + this.materialFactory = skeletonDataOrConfiguration.materialFactory ?? (() => new THREE.MeshStandardMaterial(SkeletonMesh.DEFAULT_MATERIAL_PARAMETERS)); + this.skeleton = new Skeleton(skeletonDataOrConfiguration.skeletonData); + let animData = new AnimationStateData(skeletonDataOrConfiguration.skeletonData); this.state = new AnimationState(animData); + + Object.defineProperty(this, 'castShadow', { + get: () => this._castShadow, + set: (value: boolean) => { + this._castShadow = value; + this.traverse((child) => { + if (child instanceof MeshBatcher) { + child.castShadow = value; + } + }); + }, + }); + + Object.defineProperty(this, 'receiveShadow', { + get: () => this._receiveShadow, + set: (value: boolean) => { + this._receiveShadow = value; + // Propagate to children + this.traverse((child) => { + if (child instanceof MeshBatcher) { + child.receiveShadow = value; + } + }); + }, + }); } update (deltaTime: number) { @@ -162,7 +170,7 @@ export class SkeletonMesh extends THREE.Object3D { private nextBatch () { if (this.batches.length == this.nextBatchIndex) { - let batch = new MeshBatcher(10920, this.materialCustomerizer); + let batch = new MeshBatcher(MeshBatcher.MAX_VERTICES, this.materialFactory); this.add(batch); this.batches.push(batch); } @@ -174,8 +182,6 @@ export class SkeletonMesh extends THREE.Object3D { private updateGeometry () { this.clearBatches(); - let tempPos = this.tempPos; - let tempUv = this.tempUv; let tempLight = this.tempLight; let tempDark = this.tempDark; let clipper = this.clipper; diff --git a/spine-ts/spine-threejs/src/ThreeJsTexture.ts b/spine-ts/spine-threejs/src/ThreeJsTexture.ts index 16961006fe..435e56385d 100644 --- a/spine-ts/spine-threejs/src/ThreeJsTexture.ts +++ b/spine-ts/spine-threejs/src/ThreeJsTexture.ts @@ -42,6 +42,10 @@ export class ThreeJsTexture extends Texture { // if the texture is not pma, we ask to threejs to premultiply on upload this.texture.premultiplyAlpha = !pma; this.texture.flipY = false; + + // this is necessary in future versions of ThreeJS + this.texture.colorSpace = THREE.SRGBColorSpace; + this.texture.needsUpdate = true; }