1/*
2 * Copyright (c) 2009-2010 jMonkeyEngine
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
7 * met:
8 *
9 * * Redistributions of source code must retain the above copyright
10 *   notice, this list of conditions and the following disclaimer.
11 *
12 * * Redistributions in binary form must reproduce the above copyright
13 *   notice, this list of conditions and the following disclaimer in the
14 *   documentation and/or other materials provided with the distribution.
15 *
16 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17 *   may be used to endorse or promote products derived from this software
18 *   without specific prior written permission.
19 *
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 */
32
33package com.jme3.material.plugins;
34
35import com.jme3.asset.*;
36import com.jme3.material.RenderState.BlendMode;
37import com.jme3.material.RenderState.FaceCullMode;
38import com.jme3.material.*;
39import com.jme3.material.TechniqueDef.LightMode;
40import com.jme3.material.TechniqueDef.ShadowMode;
41import com.jme3.math.ColorRGBA;
42import com.jme3.math.Vector2f;
43import com.jme3.math.Vector3f;
44import com.jme3.shader.VarType;
45import com.jme3.texture.Texture;
46import com.jme3.texture.Texture.WrapMode;
47import com.jme3.texture.Texture2D;
48import com.jme3.util.PlaceholderAssets;
49import com.jme3.util.blockparser.BlockLanguageParser;
50import com.jme3.util.blockparser.Statement;
51import java.io.IOException;
52import java.io.InputStream;
53import java.util.List;
54import java.util.logging.Level;
55import java.util.logging.Logger;
56
57public class J3MLoader implements AssetLoader {
58
59    private static final Logger logger = Logger.getLogger(J3MLoader.class.getName());
60
61    private AssetManager assetManager;
62    private AssetKey key;
63
64    private MaterialDef materialDef;
65    private Material material;
66    private TechniqueDef technique;
67    private RenderState renderState;
68
69    private String shaderLang;
70    private String vertName;
71    private String fragName;
72
73    private static final String whitespacePattern = "\\p{javaWhitespace}+";
74
75    public J3MLoader(){
76    }
77
78    private void throwIfNequal(String expected, String got) throws IOException {
79        if (expected == null)
80            throw new IOException("Expected a statement, got '"+got+"'!");
81
82        if (!expected.equals(got))
83            throw new IOException("Expected '"+expected+"', got '"+got+"'!");
84    }
85
86    // <TYPE> <LANG> : <SOURCE>
87    private void readShaderStatement(String statement) throws IOException {
88        String[] split = statement.split(":");
89        if (split.length != 2){
90            throw new IOException("Shader statement syntax incorrect" + statement);
91        }
92        String[] typeAndLang = split[0].split(whitespacePattern);
93        if (typeAndLang.length != 2){
94            throw new IOException("Shader statement syntax incorrect: " + statement);
95        }
96        shaderLang = typeAndLang[1];
97        if (typeAndLang[0].equals("VertexShader")){
98            vertName = split[1].trim();
99        }else if (typeAndLang[0].equals("FragmentShader")){
100            fragName = split[1].trim();
101        }
102    }
103
104    // LightMode <MODE>
105    private void readLightMode(String statement) throws IOException{
106        String[] split = statement.split(whitespacePattern);
107        if (split.length != 2){
108            throw new IOException("LightMode statement syntax incorrect");
109        }
110        LightMode lm = LightMode.valueOf(split[1]);
111        technique.setLightMode(lm);
112    }
113
114    // ShadowMode <MODE>
115    private void readShadowMode(String statement) throws IOException{
116        String[] split = statement.split(whitespacePattern);
117        if (split.length != 2){
118            throw new IOException("ShadowMode statement syntax incorrect");
119        }
120        ShadowMode sm = ShadowMode.valueOf(split[1]);
121        technique.setShadowMode(sm);
122    }
123
124    private Object readValue(VarType type, String value) throws IOException{
125        if (type.isTextureType()){
126//            String texturePath = readString("[\n;(//)(\\})]");
127            String texturePath = value.trim();
128            boolean flipY = false;
129            boolean repeat = false;
130            if (texturePath.startsWith("Flip Repeat ")){
131                texturePath = texturePath.substring(12).trim();
132                flipY = true;
133                repeat = true;
134            }else if (texturePath.startsWith("Flip ")){
135                texturePath = texturePath.substring(5).trim();
136                flipY = true;
137            }else if (texturePath.startsWith("Repeat ")){
138                texturePath = texturePath.substring(7).trim();
139                repeat = true;
140            }
141
142            TextureKey texKey = new TextureKey(texturePath, flipY);
143            texKey.setAsCube(type == VarType.TextureCubeMap);
144            texKey.setGenerateMips(true);
145
146            Texture tex;
147            try {
148                tex = assetManager.loadTexture(texKey);
149            } catch (AssetNotFoundException ex){
150                logger.log(Level.WARNING, "Cannot locate {0} for material {1}", new Object[]{texKey, key});
151                tex = null;
152            }
153            if (tex != null){
154                if (repeat){
155                    tex.setWrap(WrapMode.Repeat);
156                }
157            }else{
158                tex = new Texture2D(PlaceholderAssets.getPlaceholderImage());
159            }
160            return tex;
161        }else{
162            String[] split = value.trim().split(whitespacePattern);
163            switch (type){
164                case Float:
165                    if (split.length != 1){
166                        throw new IOException("Float value parameter must have 1 entry: " + value);
167                    }
168                     return Float.parseFloat(split[0]);
169                case Vector2:
170                    if (split.length != 2){
171                        throw new IOException("Vector2 value parameter must have 2 entries: " + value);
172                    }
173                    return new Vector2f(Float.parseFloat(split[0]),
174                                                               Float.parseFloat(split[1]));
175                case Vector3:
176                    if (split.length != 3){
177                        throw new IOException("Vector3 value parameter must have 3 entries: " + value);
178                    }
179                    return new Vector3f(Float.parseFloat(split[0]),
180                                                               Float.parseFloat(split[1]),
181                                                               Float.parseFloat(split[2]));
182                case Vector4:
183                    if (split.length != 4){
184                        throw new IOException("Vector4 value parameter must have 4 entries: " + value);
185                    }
186                    return new ColorRGBA(Float.parseFloat(split[0]),
187                                                                Float.parseFloat(split[1]),
188                                                                Float.parseFloat(split[2]),
189                                                                Float.parseFloat(split[3]));
190                case Int:
191                    if (split.length != 1){
192                        throw new IOException("Int value parameter must have 1 entry: " + value);
193                    }
194                    return Integer.parseInt(split[0]);
195                case Boolean:
196                    if (split.length != 1){
197                        throw new IOException("Boolean value parameter must have 1 entry: " + value);
198                    }
199                    return Boolean.parseBoolean(split[0]);
200                default:
201                    throw new UnsupportedOperationException("Unknown type: "+type);
202            }
203        }
204    }
205
206    // <TYPE> <NAME> [ "(" <FFBINDING> ")" ] [ ":" <DEFAULTVAL> ]
207    private void readParam(String statement) throws IOException{
208        String name;
209        String defaultVal = null;
210        FixedFuncBinding ffBinding = null;
211
212        String[] split = statement.split(":");
213
214        // Parse default val
215        if (split.length == 1){
216            // Doesn't contain default value
217        }else{
218            if (split.length != 2){
219                throw new IOException("Parameter statement syntax incorrect");
220            }
221            statement = split[0].trim();
222            defaultVal = split[1].trim();
223        }
224
225        // Parse ffbinding
226        int startParen = statement.indexOf("(");
227        if (startParen != -1){
228            // get content inside parentheses
229            int endParen = statement.indexOf(")", startParen);
230            String bindingStr = statement.substring(startParen+1, endParen).trim();
231            try {
232                ffBinding = FixedFuncBinding.valueOf(bindingStr);
233            } catch (IllegalArgumentException ex){
234                throw new IOException("FixedFuncBinding '" +
235                                      split[1] + "' does not exist!");
236            }
237            statement = statement.substring(0, startParen);
238        }
239
240        // Parse type + name
241        split = statement.split(whitespacePattern);
242        if (split.length != 2){
243            throw new IOException("Parameter statement syntax incorrect");
244        }
245
246        VarType type;
247        if (split[0].equals("Color")){
248            type = VarType.Vector4;
249        }else{
250            type = VarType.valueOf(split[0]);
251        }
252
253        name = split[1];
254
255        Object defaultValObj = null;
256        if (defaultVal != null){
257            defaultValObj = readValue(type, defaultVal);
258        }
259
260        materialDef.addMaterialParam(type, name, defaultValObj, ffBinding);
261    }
262
263    private void readValueParam(String statement) throws IOException{
264        // Use limit=1 incase filename contains colons
265        String[] split = statement.split(":", 2);
266        if (split.length != 2){
267            throw new IOException("Value parameter statement syntax incorrect");
268        }
269        String name = split[0].trim();
270
271        // parse value
272        MatParam p = material.getMaterialDef().getMaterialParam(name);
273        if (p == null){
274            throw new IOException("The material parameter: "+name+" is undefined.");
275        }
276
277        Object valueObj = readValue(p.getVarType(), split[1]);
278        if (p.getVarType().isTextureType()){
279            material.setTextureParam(name, p.getVarType(), (Texture) valueObj);
280        }else{
281            material.setParam(name, p.getVarType(), valueObj);
282        }
283    }
284
285    private void readMaterialParams(List<Statement> paramsList) throws IOException{
286        for (Statement statement : paramsList){
287            readParam(statement.getLine());
288        }
289    }
290
291    private void readExtendingMaterialParams(List<Statement> paramsList) throws IOException{
292        for (Statement statement : paramsList){
293            readValueParam(statement.getLine());
294        }
295    }
296
297    private void readWorldParams(List<Statement> worldParams) throws IOException{
298        for (Statement statement : worldParams){
299            technique.addWorldParam(statement.getLine());
300        }
301    }
302
303    private boolean parseBoolean(String word){
304        return word != null && word.equals("On");
305    }
306
307    private void readRenderStateStatement(String statement) throws IOException{
308        String[] split = statement.split(whitespacePattern);
309        if (split[0].equals("Wireframe")){
310            renderState.setWireframe(parseBoolean(split[1]));
311        }else if (split[0].equals("FaceCull")){
312            renderState.setFaceCullMode(FaceCullMode.valueOf(split[1]));
313        }else if (split[0].equals("DepthWrite")){
314            renderState.setDepthWrite(parseBoolean(split[1]));
315        }else if (split[0].equals("DepthTest")){
316            renderState.setDepthTest(parseBoolean(split[1]));
317        }else if (split[0].equals("Blend")){
318            renderState.setBlendMode(BlendMode.valueOf(split[1]));
319        }else if (split[0].equals("AlphaTestFalloff")){
320            renderState.setAlphaTest(true);
321            renderState.setAlphaFallOff(Float.parseFloat(split[1]));
322        }else if (split[0].equals("PolyOffset")){
323            float factor = Float.parseFloat(split[1]);
324            float units = Float.parseFloat(split[2]);
325            renderState.setPolyOffset(factor, units);
326        }else if (split[0].equals("ColorWrite")){
327            renderState.setColorWrite(parseBoolean(split[1]));
328        }else if (split[0].equals("PointSprite")){
329            renderState.setPointSprite(parseBoolean(split[1]));
330        }else{
331            throwIfNequal(null, split[0]);
332        }
333    }
334
335    private void readAdditionalRenderState(List<Statement> renderStates) throws IOException{
336        renderState = material.getAdditionalRenderState();
337        for (Statement statement : renderStates){
338            readRenderStateStatement(statement.getLine());
339        }
340        renderState = null;
341    }
342
343    private void readRenderState(List<Statement> renderStates) throws IOException{
344        renderState = new RenderState();
345        for (Statement statement : renderStates){
346            readRenderStateStatement(statement.getLine());
347        }
348        technique.setRenderState(renderState);
349        renderState = null;
350    }
351
352    // <DEFINENAME> [ ":" <PARAMNAME> ]
353    private void readDefine(String statement) throws IOException{
354        String[] split = statement.split(":");
355        if (split.length == 1){
356            // add preset define
357            technique.addShaderPresetDefine(split[0].trim(), VarType.Boolean, true);
358        }else if (split.length == 2){
359            technique.addShaderParamDefine(split[1].trim(), split[0].trim());
360        }else{
361            throw new IOException("Define syntax incorrect");
362        }
363    }
364
365    private void readDefines(List<Statement> defineList) throws IOException{
366        for (Statement statement : defineList){
367            readDefine(statement.getLine());
368        }
369
370    }
371
372    private void readTechniqueStatement(Statement statement) throws IOException{
373        String[] split = statement.getLine().split("[ \\{]");
374        if (split[0].equals("VertexShader") ||
375            split[0].equals("FragmentShader")){
376            readShaderStatement(statement.getLine());
377        }else if (split[0].equals("LightMode")){
378            readLightMode(statement.getLine());
379        }else if (split[0].equals("ShadowMode")){
380            readShadowMode(statement.getLine());
381        }else if (split[0].equals("WorldParameters")){
382            readWorldParams(statement.getContents());
383        }else if (split[0].equals("RenderState")){
384            readRenderState(statement.getContents());
385        }else if (split[0].equals("Defines")){
386            readDefines(statement.getContents());
387        }else{
388            throwIfNequal(null, split[0]);
389        }
390    }
391
392    private void readTransparentStatement(String statement) throws IOException{
393        String[] split = statement.split(whitespacePattern);
394        if (split.length != 2){
395            throw new IOException("Transparent statement syntax incorrect");
396        }
397        material.setTransparent(parseBoolean(split[1]));
398    }
399
400    private void readTechnique(Statement techStat) throws IOException{
401        String[] split = techStat.getLine().split(whitespacePattern);
402        if (split.length == 1){
403            technique = new TechniqueDef(null);
404        }else if (split.length == 2){
405            technique = new TechniqueDef(split[1]);
406        }else{
407            throw new IOException("Technique statement syntax incorrect");
408        }
409
410        for (Statement statement : techStat.getContents()){
411            readTechniqueStatement(statement);
412        }
413
414        if (vertName != null && fragName != null){
415            technique.setShaderFile(vertName, fragName, shaderLang);
416        }
417
418        materialDef.addTechniqueDef(technique);
419        technique = null;
420        vertName = null;
421        fragName = null;
422        shaderLang = null;
423    }
424
425    private void loadFromRoot(List<Statement> roots) throws IOException{
426        if (roots.size() == 2){
427            Statement exception = roots.get(0);
428            String line = exception.getLine();
429            if (line.startsWith("Exception")){
430                throw new AssetLoadException(line.substring("Exception ".length()));
431            }else{
432                throw new IOException("In multiroot material, expected first statement to be 'Exception'");
433            }
434        }else if (roots.size() != 1){
435            throw new IOException("Too many roots in J3M/J3MD file");
436        }
437
438        boolean extending = false;
439        Statement materialStat = roots.get(0);
440        String materialName = materialStat.getLine();
441        if (materialName.startsWith("MaterialDef")){
442            materialName = materialName.substring("MaterialDef ".length()).trim();
443            extending = false;
444        }else if (materialName.startsWith("Material")){
445            materialName = materialName.substring("Material ".length()).trim();
446            extending = true;
447        }else{
448            throw new IOException("Specified file is not a Material file");
449        }
450
451        String[] split = materialName.split(":", 2);
452
453        if (materialName.equals("")){
454            throw new IOException("Material name cannot be empty");
455        }
456
457        if (split.length == 2){
458            if (!extending){
459                throw new IOException("Must use 'Material' when extending.");
460            }
461
462            String extendedMat = split[1].trim();
463
464            MaterialDef def = (MaterialDef) assetManager.loadAsset(new AssetKey(extendedMat));
465            if (def == null)
466                throw new IOException("Extended material "+extendedMat+" cannot be found.");
467
468            material = new Material(def);
469            material.setKey(key);
470//            material.setAssetName(fileName);
471        }else if (split.length == 1){
472            if (extending){
473                throw new IOException("Expected ':', got '{'");
474            }
475            materialDef = new MaterialDef(assetManager, materialName);
476            // NOTE: pass file name for defs so they can be loaded later
477            materialDef.setAssetName(key.getName());
478        }else{
479            throw new IOException("Cannot use colon in material name/path");
480        }
481
482        for (Statement statement : materialStat.getContents()){
483            split = statement.getLine().split("[ \\{]");
484            String statType = split[0];
485            if (extending){
486                if (statType.equals("MaterialParameters")){
487                    readExtendingMaterialParams(statement.getContents());
488                }else if (statType.equals("AdditionalRenderState")){
489                    readAdditionalRenderState(statement.getContents());
490                }else if (statType.equals("Transparent")){
491                    readTransparentStatement(statement.getLine());
492                }
493            }else{
494                if (statType.equals("Technique")){
495                    readTechnique(statement);
496                }else if (statType.equals("MaterialParameters")){
497                    readMaterialParams(statement.getContents());
498                }else{
499                    throw new IOException("Expected material statement, got '"+statType+"'");
500                }
501            }
502        }
503    }
504
505    public Object load(AssetInfo info) throws IOException {
506        this.assetManager = info.getManager();
507
508        InputStream in = info.openStream();
509        try {
510            key = info.getKey();
511            loadFromRoot(BlockLanguageParser.parse(in));
512        } finally {
513            if (in != null){
514                in.close();
515            }
516        }
517
518        if (material != null){
519            if (!(info.getKey() instanceof MaterialKey)){
520                throw new IOException("Material instances must be loaded via MaterialKey");
521            }
522            // material implementation
523            return material;
524        }else{
525            // material definition
526            return materialDef;
527        }
528    }
529
530}
531